coffeescript source code of the animated coalition bar chart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coalition stacked bar chart | |
$ () -> | |
_coalitions = ["CDU,SPD", "CDU,FDP", "CDU,GRÜNE", "SPD,GRÜNE", "SPD,FDP", "SPD,LINKE", "CDU,FDP,GRÜNE", "SPD,FDP,GRÜNE", "SPD,LINKE,GRÜNE"] | |
_cont = $ '#canvas' | |
_lblcont = $ '.vis-coalitions' | |
canvas = Raphael _cont.get 0 | |
bar_w = 30 | |
elections = null | |
width = _cont.width() | |
height = _cont.height()-12 | |
stacked_bars = {} | |
grid = {} | |
bg = null | |
partyColors = Common.partyColors | |
winner = | |
'13': 'SPD,GRÜNE' | |
'08': 'CDU,FDP' | |
'03': 'CDU,FDP' | |
'98': 'SPD' | |
'94': 'SPD' | |
'90': 'SPD,GRÜNE' | |
$.fn.set = (txt) -> | |
$('span', this).html txt | |
get_coalitions = (election, justParties=false) -> | |
coalitions = [] | |
election.min_seats = Math.ceil((election.s + 0.5) * 0.5) | |
$.each _coalitions, (i) -> | |
coalition = | |
id: i | |
key: _coalitions[i] | |
parties: [] | |
seats: 0 | |
$.each _coalitions[i].split(','), (p, party) -> | |
if election.result[party] and election.result[party].s > 0 # if party has results | |
coalition.parties.push # add it to coalition | |
name: party | |
seats: election.result[party].s | |
# and add their seats to the coalition seats | |
coalition.seats += election.result[party].s | |
else | |
coalition = null | |
return false | |
if coalition? | |
# sort parties in coalition by seats | |
coalition.parties.sort (a,b) -> | |
b.seats - a.seats | |
coalitions.push coalition | |
# filter coalitions that make no sense | |
coalitions = coalitions.filter (coalition) -> | |
if coalition.parties.length == 3 | |
makessense = true | |
$.each coalitions, (i, c) -> | |
if c.parties.length == 2 | |
if c.parties[0].name == coalition.parties[0].name | |
if c.parties[1].name == coalition.parties[1].name or c.parties[1].name == coalition.parties[2].name | |
if c.seats > election.min_seats | |
makessense = false | |
return false | |
makessense | |
else | |
true | |
if justParties | |
coalitions = [] | |
# add individual parties for comparison | |
$.each election.result, (party) -> | |
if election.result[party].s > election.min_seats * 0.5 or (justParties and election.result[party].s > 0) | |
coalitions.push | |
key: party | |
seats: election.result[party].s | |
parties: [{ name: party, seats: election.result[party].s }] | |
# sort coalitions by majority | |
coalitions.sort (a,b) -> | |
if a.seats >= election.min_seats && b.seats >= election.min_seats | |
# sort by biggest party first, bc they have the | |
# privilege to set a government | |
if a.parties[0].seats != b.parties[0].seats | |
return b.parties[0].seats - a.parties[0].seats | |
b.seats - a.seats; | |
# return coalitions | |
coalitions | |
render = (election_index, justParties=false) -> | |
election = elections[election_index] | |
coalitions = get_coalitions election, justParties | |
bar_w = if justParties then 60 else 30 | |
yscale = (seats) -> | |
# scale according to biggest coalition seats | |
Math.round seats / coalitions[0].seats * height | |
label = (clss, txt) -> | |
lbl = $ '<div class="label '+clss+'"><span /></div>' | |
_lblcont.append lbl | |
lbl.css | |
opacity: 0 | |
if txt? | |
lbl.set txt | |
lbl | |
if not justParties | |
$('.desc').hide() | |
setTimeout () -> | |
$('.desc').fadeIn 1000 | |
, 2000 | |
$('.label.top').animate | |
opacity: 0 | |
bar_w = Math.round((width - 220) / coalitions.length / 1.8) | |
offset = width - coalitions.length * bar_w * 1.8 - 15 | |
if justParties | |
bar_w = (width - 340) / coalitions.length / 1.8 | |
# animate bars for each coalition | |
possible = true | |
$.each coalitions, (i, coalition) -> | |
if not stacked_bars[coalition.key]? | |
# first occurance of this coalition, create bars | |
sbc = stacked_bars[coalition.key] = {} | |
$.each coalition.parties, (j, party) -> | |
x = 20 + i * bar_w * 1.8 | |
x += 340 if justParties | |
y = 0 | |
h = 0 | |
bar = canvas.rect x, height - h - y, bar_w, h | |
bar.attr | |
stroke: if coalition.seats > election.min_seats then '#fff' else '#eee' | |
opacity: 0.98 | |
fill: partyColors[party.name] || ('#ccc' && console.log(party.name)) | |
sbc[party.name] = bar | |
sbc.toplabel = label 'bar top center' | |
sbc.bottomlabel = label 'bar bottom center', coalition.key.replace(/,/g, '<br/>') | |
sbc = stacked_bars[coalition.key] | |
# position the stacked bars | |
y = 0 | |
x = Math.round 20 + i * bar_w * 1.8 | |
if coalition.seats < election.min_seats | |
x += offset if not justParties | |
if possible | |
$('.desc-impossible').css | |
right: width - x+10 | |
$('.desc-possible').css | |
left: x - offset | |
possible = false | |
if justParties | |
# move to the right | |
x += 340 | |
setTimeout () -> | |
sbc.bottomlabel.animate | |
left: x-40+bar_w*0.5 | |
sbc.toplabel.animate | |
left: x-40+bar_w*0.5 | |
, 1000 | |
# make labels visible again, in case they're hidden | |
setTimeout () -> | |
sbc.bottomlabel.animate | |
opacity: 1 | |
sbc.toplabel.animate | |
opacity: 1 | |
, if justParties then 1000 else 2000 | |
$.each coalition.parties, (j, party) -> | |
bar = sbc[party.name] | |
# animate bar heights and y position first | |
h = yscale party.seats | |
barprops = | |
y: height - h - y - 0.5 | |
height: h | |
width: bar_w | |
bar.animate barprops, 1000, 'expoInOut', () -> | |
props = # animate stacks x position last | |
bar.animate | |
x: x + 0.5 | |
, 1000, 'expoInOut' | |
# make bar visible again, it case it was hidden | |
setTimeout ()-> | |
bar.animate | |
opacity: 1 | |
, 300 | |
, 1000 | |
y += h | |
sbc.toplabel.animate | |
top: height - y - 22 | |
if justParties | |
tl = coalition.seats | |
sbc.toplabel.removeClass 'negative' | |
else | |
tl = coalition.seats - election.min_seats | |
if tl < 0 | |
sbc.toplabel.addClass 'negative' | |
else | |
sbc.toplabel.removeClass 'negative' | |
tl = '+' + tl if tl > 0 | |
tl = '±' + tl if tl == 0 | |
sbc.toplabel.set tl | |
$('.label.bottom').removeClass 'winner' | |
setTimeout () -> | |
if not justParties | |
if winner[election.yr] and stacked_bars[winner[election.yr]] | |
stacked_bars[winner[election.yr]].bottomlabel.addClass 'winner' | |
, 2200 | |
# hide previous coalitions | |
$.each stacked_bars, (key, bars) -> | |
found = false | |
$.each coalitions, (i, coalition) -> | |
if coalition.key == key | |
found = true | |
false | |
true | |
if not found | |
$.each bars, (party, bar) -> | |
bar.animate | |
opacity: 0 | |
, 300 | |
# update grid lines | |
init_grid_line = (addlbl=true) -> | |
line = canvas.path 'M0,'+(height-.5)+' L'+width+','+(height-0.5) | |
line.toBack() | |
if addlbl | |
lbl = label 'grid left' | |
line.data 'lbl', lbl | |
lbl = label 'grid right' | |
line.data 'lbl2', lbl | |
line | |
move_grid_line = (line, seats) -> | |
animprops = | |
transform: 't0,'+(0 - yscale(seats)) | |
line.animate animprops, '800', 'expoInOut' | |
if line.data('lbl')? | |
lbl = line.data 'lbl' | |
lbl.set seats | |
lbl.animate | |
opacity: 1 | |
top: height - yscale(seats) - 8 | |
lbl = line.data 'lbl2' | |
lbl.set seats | |
lbl.animate | |
opacity: 1 | |
top: height - yscale(seats) - 8 | |
return | |
if not grid.min_seats? | |
grid.min_seats = init_grid_line() | |
grid.min_seats.data('lbl').addClass 'min-seats' | |
grid.min_seats.data('lbl2').addClass 'min-seats' | |
# initialize base line | |
grid.bottom = init_grid_line(false) #.transform 't0,1' | |
grid.bottom.toFront() | |
move_grid_line grid.min_seats, election.min_seats | |
# init and move grid lines | |
$.each [20,40,60,80,100], (i,seats) -> | |
if not grid[seats]? | |
grid[seats] = init_grid_line() | |
grid[seats].attr | |
'stroke-dasharray': '- ' | |
move_grid_line grid[seats], seats | |
lineprops = | |
opacity: if seats+5 < election.min_seats then 1 else 0 | |
grid[seats].animate lineprops, 500 | |
grid[seats].data('lbl').animate lineprops | |
grid[seats].data('lbl2').animate lineprops | |
# init and resize background | |
if not bg? | |
bg = canvas.rect 0,height-1,width,1 | |
bg.attr | |
fill: '#f5f5f5' | |
stroke: false | |
bg.toBack() | |
bgprops = | |
y: height - yscale(election.min_seats) | |
height: yscale(election.min_seats) | |
opacity: 1 | |
if justParties | |
$('.desc-intro').show() | |
$('.desc-impossible, .desc-possible, .label.left').hide() | |
bg.animate | |
opacity: 0 | |
else | |
$('.desc-intro').hide() | |
$('.desc-impossible, .desc-possible, .label.left').show() | |
bg.animate bgprops, 800, 'expoInOut' | |
if location.hash.length and location.hash != '#activate' | |
progn = location.hash.substr(1).replace(/\//, '-') | |
else | |
#progn = $($('.prognosen a').get(0)).attr('href').substr(1) | |
progn = 'elections-nds-amtl-2342' | |
$('.prognosen a[href=#'+progn+']').addClass 'active' | |
$.ajax | |
url: '/assets/data/'+progn+'.json' | |
dataType: 'json' | |
.done (json) -> | |
elections = json | |
active = elections.length-1 | |
elections[active].s = 0 | |
for p of elections[active].result | |
elections[active].s += Number(elections[active].result[p].s) | |
justParties = location.hash.length == 0 | |
elsel = Common.ElectionSelector elections, active | |
, (active) -> # click callback | |
$('.prognosen a').removeClass 'active' | |
render active, justParties | |
true | |
, (e) -> # function that extracts year | |
e.yr | |
$('[href=#activate]').click ()-> | |
justParties = false | |
render active, justParties | |
render active, justParties | |
blocked = false | |
$('.prognosen a').click (evt) -> | |
if blocked | |
return | |
blocked = true | |
evt.preventDefault() | |
a = $(evt.target) | |
prog = a.attr('href').substr(1) | |
$('.prognosen a').removeClass 'active' | |
$.getJSON '/assets/data/' + prog + '.json', (data) -> | |
latest = data[data.length-1] | |
different = false | |
latest.s = 0 | |
for p of latest.result | |
if elections[elections.length-1].result[p].s != latest.result[p].s | |
different = true | |
latest.s += Number(latest.result[p].s) | |
elections[elections.length-1] = latest | |
if different | |
render active, justParties | |
a.addClass 'active' | |
location.hash = '#' + prog | |
blocked = false | |
# location.href = '/koalitionen/' + $(evt.target).attr('href') | |
# location.reload() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[{ | |
"d": 100, | |
"eligable-voters": 5712613, | |
"voters": 4263215, | |
"i": [58983, 46919], | |
"s": 155, | |
"result": { | |
"FBPD": { | |
"s": 0, | |
"d": 0, | |
"v": [81, 0] | |
}, | |
"Bewu\u00dftsein": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 632] | |
}, | |
"CM": { | |
"s": 0, | |
"d": 0, | |
"v": [90, 1367] | |
}, | |
"FDP": { | |
"s": 9, | |
"d": 0, | |
"v": [213425, 252615] | |
}, | |
"DS": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 525] | |
}, | |
"NPD": { | |
"s": 0, | |
"d": 0, | |
"v": [9396, 8255] | |
}, | |
"REP": { | |
"s": 0, | |
"d": 0, | |
"v": [622, 62054] | |
}, | |
"FVP": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 309] | |
}, | |
"Die Unabh\u00e4ngigen": { | |
"s": 0, | |
"d": 0, | |
"v": [5737, 2739] | |
}, | |
"\u00d6KO-UNION": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 3682] | |
}, | |
"CDU": { | |
"s": 67, | |
"d": 39, | |
"v": [1811352, 1771974] | |
}, | |
"Familie": { | |
"s": 0, | |
"d": 0, | |
"v": [531, 4529] | |
}, | |
"PBC": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 3858] | |
}, | |
"Einzelbewerber": { | |
"s": 0, | |
"d": 0, | |
"v": [208, 0] | |
}, | |
"Patrioten": { | |
"s": 0, | |
"d": 0, | |
"v": [421, 650] | |
}, | |
"DDD": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 1126] | |
}, | |
"SPD": { | |
"s": 71, | |
"d": 61, | |
"v": [1943278, 1865267] | |
}, | |
"DRD": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 2705] | |
}, | |
"DP": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 560] | |
}, | |
"GR\u00dcNE": { | |
"s": 8, | |
"d": 0, | |
"v": [213114, 229846] | |
} | |
}, | |
"v": [4204232, 4216296], | |
"yr": "90" | |
}, { | |
"d": 100, | |
"eligable-voters": 5851720, | |
"voters": 4316428, | |
"i": [85427, 67407], | |
"s": 161, | |
"result": { | |
"\u00d6DP": { | |
"s": 0, | |
"d": 0, | |
"v": [3093, 4347] | |
}, | |
"REP": { | |
"s": 0, | |
"d": 0, | |
"v": [95902, 159026] | |
}, | |
"STATT Partei": { | |
"s": 0, | |
"d": 0, | |
"v": [43803, 55605] | |
}, | |
"FDP": { | |
"s": 0, | |
"d": 0, | |
"v": [174743, 188691] | |
}, | |
"DP": { | |
"s": 0, | |
"d": 0, | |
"v": [2263, 0] | |
}, | |
"NEUE STATT PARTEI": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 19361] | |
}, | |
"LLN": { | |
"s": 0, | |
"d": 0, | |
"v": [1429, 8176] | |
}, | |
"SPD": { | |
"s": 81, | |
"d": 81, | |
"v": [1971551, 1880623] | |
}, | |
"WIR": { | |
"s": 0, | |
"d": 0, | |
"v": [136, 0] | |
}, | |
"CM": { | |
"s": 0, | |
"d": 0, | |
"v": [1232, 0] | |
}, | |
"NATURGESETZ": { | |
"s": 0, | |
"d": 0, | |
"v": [431, 7325] | |
}, | |
"PBC": { | |
"s": 0, | |
"d": 0, | |
"v": [908, 8152] | |
}, | |
"Einzelbewerber": { | |
"s": 0, | |
"d": 0, | |
"v": [4411, 0] | |
}, | |
"B\u00fcSo": { | |
"s": 0, | |
"d": 0, | |
"v": [456, 0] | |
}, | |
"NPD": { | |
"s": 0, | |
"d": 0, | |
"v": [2562, 9430] | |
}, | |
"MITTE": { | |
"s": 0, | |
"d": 0, | |
"v": [5155, 4123] | |
}, | |
"Die Unabh\u00e4ngigen": { | |
"s": 0, | |
"d": 0, | |
"v": [4000, 4906] | |
}, | |
"\u00d6KO-UNION": { | |
"s": 0, | |
"d": 0, | |
"v": [1541, 7902] | |
}, | |
"CDU": { | |
"s": 67, | |
"d": 19, | |
"v": [1610098, 1547610] | |
}, | |
"UWN": { | |
"s": 0, | |
"d": 0, | |
"v": [11140, 8819] | |
}, | |
"GRAUE": { | |
"s": 0, | |
"d": 0, | |
"v": [2304, 20581] | |
}, | |
"GR\u00dcNE": { | |
"s": 13, | |
"d": 0, | |
"v": [293837, 314344] | |
} | |
}, | |
"v": [4231001, 4249021], | |
"yr": "94" | |
}, { | |
"d": 100, | |
"eligable-voters": 5929243, | |
"voters": 4376643, | |
"i": [80051, 61711], | |
"s": 157, | |
"result": { | |
"\u00f6dp": { | |
"s": 0, | |
"d": 0, | |
"v": [2587, 4730] | |
}, | |
"DKP": { | |
"s": 0, | |
"d": 0, | |
"v": [1331, 8597] | |
}, | |
"CDU": { | |
"s": 62, | |
"d": 17, | |
"v": [1647814, 1549227] | |
}, | |
"REP": { | |
"s": 0, | |
"d": 0, | |
"v": [41557, 118975] | |
}, | |
"STATT Partei": { | |
"s": 0, | |
"d": 0, | |
"v": [29727, 30224] | |
}, | |
"PDS": { | |
"s": 0, | |
"d": 0, | |
"v": [6504, 0] | |
}, | |
"DIE FRAUEN": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 6775] | |
}, | |
"SFP": { | |
"s": 0, | |
"d": 0, | |
"v": [602, 0] | |
}, | |
"FDP": { | |
"s": 0, | |
"d": 0, | |
"v": [143702, 209610] | |
}, | |
"PBC": { | |
"s": 0, | |
"d": 0, | |
"v": [2724, 7984] | |
}, | |
"Einzelbewerber": { | |
"s": 0, | |
"d": 0, | |
"v": [14948, 0] | |
}, | |
"SPD": { | |
"s": 83, | |
"d": 83, | |
"v": [2090805, 2068477] | |
}, | |
"DP": { | |
"s": 0, | |
"d": 0, | |
"v": [4087, 6140] | |
}, | |
"GR\u00dcNE": { | |
"s": 12, | |
"d": 0, | |
"v": [310204, 304193] | |
} | |
}, | |
"v": [4296592, 4314932], | |
"yr": "98" | |
}, { | |
"d": 100, | |
"eligable-voters": 6023636, | |
"voters": 4036017, | |
"i": [58812, 52008], | |
"s": 183, | |
"result": { | |
"\u00f6dp": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 3671] | |
}, | |
"FDP": { | |
"s": 15, | |
"d": 0, | |
"v": [176862, 323107] | |
}, | |
"B\u00fcSo": { | |
"s": 0, | |
"d": 0, | |
"v": [1455, 0] | |
}, | |
"REP": { | |
"s": 0, | |
"d": 0, | |
"v": [1152, 17043] | |
}, | |
"PDS": { | |
"s": 0, | |
"d": 0, | |
"v": [14605, 21560] | |
}, | |
"CDU": { | |
"s": 91, | |
"d": 91, | |
"v": [2077689, 1925055] | |
}, | |
"FAMILIE": { | |
"s": 0, | |
"d": 0, | |
"v": [868, 0] | |
}, | |
"PBC": { | |
"s": 0, | |
"d": 0, | |
"v": [2984, 7819] | |
}, | |
"Einzelbewerber": { | |
"s": 0, | |
"d": 0, | |
"v": [5197, 0] | |
}, | |
"GRAUE": { | |
"s": 0, | |
"d": 0, | |
"v": [589, 10724] | |
}, | |
"SPD": { | |
"s": 63, | |
"d": 9, | |
"v": [1441971, 1330156] | |
}, | |
"Schill": { | |
"s": 0, | |
"d": 0, | |
"v": [19419, 40342] | |
}, | |
"GR\u00dcNE": { | |
"s": 14, | |
"d": 0, | |
"v": [234414, 304532] | |
} | |
}, | |
"v": [3977205, 3984009], | |
"yr": "03" | |
}, { | |
"d": 87, | |
"eligable-voters": 6087297, | |
"voters": 3476112, | |
"i": [61595, 50686], | |
"s": 152, | |
"result": { | |
"LINKE": { | |
"s": 11, | |
"d": 0, | |
"v": [217345, 243361] | |
}, | |
"\u00f6dp": { | |
"s": 0, | |
"d": 0, | |
"v": [487, 1962] | |
}, | |
"Die Weissen": { | |
"s": 0, | |
"d": 0, | |
"v": [519, 0] | |
}, | |
"FDP": { | |
"s": 13, | |
"d": 0, | |
"v": [191840, 279826] | |
}, | |
"NPD": { | |
"s": 0, | |
"d": 0, | |
"v": [26012, 52986] | |
}, | |
"REP": { | |
"s": 0, | |
"d": 0, | |
"v": [897, 0] | |
}, | |
"FW": { | |
"s": 0, | |
"d": 0, | |
"v": [31195, 17960] | |
}, | |
"FAMILIE": { | |
"s": 0, | |
"d": 0, | |
"v": [381, 13325] | |
}, | |
"CDU": { | |
"s": 68, | |
"d": 68, | |
"v": [1512779, 1456742] | |
}, | |
"Die Tierschutzpartei": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 17174] | |
}, | |
"Volksabstimmung": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 5934] | |
}, | |
"PBC": { | |
"s": 0, | |
"d": 0, | |
"v": [568, 5851] | |
}, | |
"Einzelbewerber": { | |
"s": 0, | |
"d": 0, | |
"v": [5033, 0] | |
}, | |
"GRAUE": { | |
"s": 0, | |
"d": 0, | |
"v": [0, 9288] | |
}, | |
"SPD": { | |
"s": 48, | |
"d": 19, | |
"v": [1183758, 1036727] | |
}, | |
"Die Friesen": { | |
"s": 0, | |
"d": 0, | |
"v": [4122, 10069] | |
}, | |
"GR\u00dcNE": { | |
"s": 12, | |
"d": 0, | |
"v": [239581, 274221] | |
} | |
}, | |
"v": [3414517, 3425426], | |
"yr": "08" | |
}, { | |
"d": 87, | |
"eligable-voters": 6100218, | |
"voters": 3620994, | |
"i": [51980, 51980], | |
"s": 147, | |
"result": { | |
"CDU": { | |
"s": 54 | |
}, | |
"FDP": { | |
"s": 14 | |
}, | |
"LINKE": { | |
"s": 0 | |
}, | |
"SPD": { | |
"s": 49 | |
}, | |
"GR\u00dcNE": { | |
"s": 20 | |
} | |
}, | |
"v": [3414517, 3425426], | |
"yr": "13" | |
}] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment