/* Vizualization for October, 2008 Canadian Federal Election (c) Patrick Dinnen, 2008 pdinnen@gmail.com v1.01 - definitely Alpha quality code scaled blob per candidate with a ring to indicate total losing voters in riding. Losing blobs arranged in a pattern around the edge of the winning blob. blob charts nicely positioned out long horizontal page version - loading from pre-generated PNG of the charts with scrollbar to move through the complete data improved rollovers, with accurate positioning and selection highlighting first release - shallower form, notes in image etc. note: doesn't do sorting, so for meaningful charts having the data source sorted appropriately is important */ HScrollbar hScroll; boolean scrolling; // tracks whether the mouse has been pressed in the scrollbar area float scrollRatio; int imageWidth; DetailPopup detailPopup; PFont largeFont, largeFontBold, smallFont, smallFontBold; Candidate[] candidates; Riding[] ridings = new Riding[308]; PImage blobChartsImage; PImage highlightScreen; Riding lastHoveredRiding; boolean chartHover; int viewportWidth = 930; int viewportHeight = 600; int loserAlphaVal = 200; // doesn't really work as hoped, 2 frames is too short a snap shot I think // int hoverMovementThreshold = 5; // distance mouse position must remain within from one frame to next to count as a hover int hoverMovementThreshold = 100; // dummy value int ridingsAdded; int recordCount; int candidateCount; Circle circle1, circle2, circle3, circle4, circle5, circle6, circle7, circleA, circleB; int votesPerPixel = 10000; float[] scaleFactors = {.15, .2, .25, .3, .35, .4, .45, .6, .75, 1.0}; int scaleIndex = 5; float scaleFactor = scaleFactors[scaleFactors.length-1]; color conColor = color(0,0,240); color conColorFaded = color(0,0,240,loserAlphaVal); color libColor = color(203,29,30); color libColorFaded = color(203,29,30,loserAlphaVal); color bqColor = color(10,136,191); color bqColorFaded = color(10,136,191,loserAlphaVal); color ndpColor = color(238,108,10); color ndpColorFaded = color(238,108,10,loserAlphaVal); color grnColor = color(71,178,25); color grnColorFaded = color(71,178,25,loserAlphaVal); color indColor = color(66); color indColorFaded = color(66,loserAlphaVal); color othColor = color(33); color othColorFaded = color(33,loserAlphaVal); int winningVotes, conWinningVotes, libWinningVotes, bqWinningVotes, ndpWinningVotes, grnWinningVotes, othWinningVotes, indWinningVotes, losingVotes, conLosingVotes, libLosingVotes, bqLosingVotes, ndpLosingVotes, grnLosingVotes, othLosingVotes, indLosingVotes; void setup() { // size(8150,530); // horizontal version size(930, 600); hScroll = new HScrollbar(0, 10, width-1, 14, 1); frameRate(20); smooth(); largeFont = loadFont("HelveticaNeue-15.vlw"); largeFontBold = loadFont("HelveticaNeue-Bold-15.vlw"); smallFont = loadFont("HelveticaNeue-12.vlw"); smallFontBold = loadFont("HelveticaNeue-Bold-12.vlw"); loadCandidates(); assignCandidatesToRidings(); findWinners(); totalVotes(); blobChartsImage = loadImage("data/cdn_election_2008.png"); imageWidth = blobChartsImage.width; detailPopup = new DetailPopup(); highlightScreen = loadImage("data/highlight_screen.png"); // we're using the pre-gen data, but some calculations we need happen in visualizeData() so it needs to run once visualizeData(); } void draw() { background(250); scrollRatio = hScroll.getPosRatio(); float scrollPos = hScroll.getPos(); scale(scaleFactor); // position the image according to the state of the scrollbar image(blobChartsImage, (-imageWidth*scrollRatio), 20); hScroll.update(); hScroll.draw(); if ( scrolling ) { detailPopup.hide(); } detailPopup.setOffset(int(imageWidth*scrollRatio),0); detailPopup.draw(); // only check for rollover when not scrolling with the scrollbar if ( !scrolling && (pmouseX - mouseX) < hoverMovementThreshold && (pmouseY - mouseY) < hoverMovementThreshold ) { checkForRidingHover(int(imageWidth*scrollRatio), -20); } // visualizeData(); } void checkForRidingHover(int xPosOffset, int yPosOffset) { int hoverThreshold = 50; for (int i=0; i < ridings.length; i++) { Riding thisRiding = ridings[i]; // if the mouse is hovering over a riding chart if ( dist(mouseX + xPosOffset, mouseY + yPosOffset, thisRiding.chartCentreX, thisRiding.chartCentreY) <= hoverThreshold ) { resetMatrix(); translate(-xPosOffset, 0); int largestBlobRadius; if ( thisRiding.loserBlobRadius > thisRiding.getWinnerRadius() ) { largestBlobRadius = thisRiding.loserBlobRadius; } else { largestBlobRadius = thisRiding.getWinnerRadius(); } fill(0); if ( !detailPopup.showing ) { textFont(smallFont, 12); text(thisRiding.ridingName, thisRiding.chartCentreX - 25, thisRiding.chartCentreY - largestBlobRadius - 5 - yPosOffset); } if ( lastHoveredRiding != thisRiding ) { // println("hovering:: " + thisRiding.ridingName); } lastHoveredRiding = thisRiding; chartHover = true; return; } else { chartHover = false; // println(dist(mouseX, mouseY, thisRiding.chartCentreX, thisRiding.chartCentreY)); } } } void keyPressed() { if ( key == 'p' ) { println("saving..."); save("data/cdn_election_2008.png"); } } void mouseClicked() { if ( chartHover && !detailPopup.showing ) { detailPopup.show( lastHoveredRiding ); } else if ( detailPopup.showing ) { detailPopup.clicked(mouseX, mouseY); } } void loadCandidates() { String[] lines = loadStrings("data/FedElectionVizData2008.tsv"); candidates = new Candidate[lines.length]; for (int i = 0; i < lines.length; i++) { String[] pieces = split(lines[i], '\t'); // Load data into array if (pieces.length == 10) { candidates[recordCount] = new Candidate(pieces); recordCount++; } } } void assignCandidatesToRidings() { candidateCount = candidates.length; for (int i=0; i < candidateCount; i++) { Candidate thisCandidate = candidates[i]; int candidateRidingNum = thisCandidate.districtNumber; boolean candidateAssigned = false; for (int j=0; j < ridings.length; j++) { if ( ridings[j] != null ) { if ( ridings[j].districtNumber == candidateRidingNum ) { ridings[j].addCandidate(thisCandidate); candidateAssigned = true; } } } if ( !candidateAssigned ) { Riding newRiding = new Riding(thisCandidate.districtNumber, thisCandidate.ridingName); newRiding.addCandidate(thisCandidate); ridings[ridingsAdded] = newRiding; ridingsAdded++; } } } void findWinners() { for (int i=0; i < ridings.length; i++) { Candidate leadingCandidate = null; Riding thisRiding = ridings[i]; int candidateCount = thisRiding.candidates.size(); for (int j=0; j < candidateCount; j++) { Candidate thisCandidate = (Candidate)thisRiding.candidates.get(j); if ( leadingCandidate == null ) { leadingCandidate = thisCandidate; } else if ( thisCandidate.votesObtained > leadingCandidate.votesObtained ) { leadingCandidate = thisCandidate; } } leadingCandidate.winner = true; } } void totalVotes() { for (int i=0; i < candidateCount; i++) { Candidate thisCandidate = candidates[i]; String party = thisCandidate.party; if ( thisCandidate.winner ) { winningVotes = winningVotes + thisCandidate.votesObtained; if ( party.equals("Conservative") ) { conWinningVotes = conWinningVotes + thisCandidate.votesObtained; } else if ( party.equals("Liberal") ) { libWinningVotes = libWinningVotes + thisCandidate.votesObtained; } else if ( party.equals("NDP") ) { ndpWinningVotes = ndpWinningVotes + thisCandidate.votesObtained; } else if ( party.equals("Bloc") ) { bqWinningVotes = bqWinningVotes + thisCandidate.votesObtained; } else if ( party.equals("Green Party") ) { grnWinningVotes = grnWinningVotes + thisCandidate.votesObtained; } else if ( party.equals("Independent") ) { indWinningVotes = indWinningVotes + thisCandidate.votesObtained; } else { othWinningVotes = othWinningVotes + thisCandidate.votesObtained; } } else { losingVotes = losingVotes + thisCandidate.votesObtained; if ( party.equals("Conservative") ) { conLosingVotes = conLosingVotes + thisCandidate.votesObtained; } else if ( party.equals("Liberal") ) { libLosingVotes = libLosingVotes + thisCandidate.votesObtained; } else if ( party.equals("NDP") ) { ndpLosingVotes = ndpLosingVotes + thisCandidate.votesObtained; } else if ( party.equals("Bloc") ) { bqLosingVotes = bqLosingVotes + thisCandidate.votesObtained; } else if ( party.equals("Green Party") ) { grnLosingVotes = grnLosingVotes + thisCandidate.votesObtained; } else if ( party.equals("Independent") ) { indLosingVotes = indLosingVotes + thisCandidate.votesObtained; } else { othLosingVotes = othLosingVotes + thisCandidate.votesObtained; } } } } void visualizeData() { Riding thisRiding; int xMargin = 10; int yMargin = 10; int xSpacing = 130; int ySpacing = 100; int xOffset = 0; int yOffset = 0; noStroke(); ellipseMode(CENTER); for (int h=0; h < ridings.length; h++) { // a hackish way of getting the positioning of the first chart right if ( h == 0 ) { translate(xMargin, yMargin); } thisRiding = ridings[h]; int candidateCount = thisRiding.candidates.size(); int votes; int circleArea; float circleRadius; float[] intersectionCoordinates; float circleCentreX = 50; float circleCentreY = 50; float circleCentreXOriginal = 50; float circleCentreYOriginal = 50; int dividerFactor = 10; float winnerRadius = sqrt((thisRiding.getWinner().votesObtained / dividerFactor) / PI); float lastCircleEdgeX = 0; // draw a scaled, coloured blob for each of the candidates in this riding for (int i=0; i < candidateCount; i++) { Candidate thisCandidate; thisCandidate = (Candidate)thisRiding.candidates.get(i); votes = thisCandidate.votesObtained; /* print(" - " + thisCandidate.party + " " + thisCandidate.votesObtained + " [" + thisCandidate.percentageVotesObtained + "%]"); */ circleArea = votes / dividerFactor; circleRadius = sqrt(circleArea / PI); thisCandidate.setBlobRadius((int)circleRadius); fill(thisCandidate.getPartyColor()); if ( thisCandidate.winner ) { ellipse(circleCentreX,circleCentreY,circleRadius*2,circleRadius*2); circle1 = new Circle(circleCentreX, circleCentreY, circleRadius); // println(thisRiding.ridingName + " - " + int(circleCentreX+xOffset+xMargin) + ", " + int(circleCentreY+yOffset+yMargin)); thisRiding.setChartCentre(int(circleCentreX+xOffset+xMargin), int(circleCentreY+yOffset+yMargin)); } else if ( i == 1 ) { circleCentreX = circleCentreXOriginal + winnerRadius + circleRadius; circleCentreY = circleCentreYOriginal; ellipse(circleCentreX,circleCentreY,circleRadius*2,circleRadius*2); circle2 = new Circle(circleCentreX, circleCentreY, circleRadius); } else if ( i == 2 ) { circleCentreX = circleCentreXOriginal + winnerRadius + circleRadius; circleCentreY = circleCentreYOriginal; circle3 = new Circle(circleCentreX, circleCentreY, circleRadius); } else if ( i == 3 ) { circleCentreX = circleCentreXOriginal; circleCentreY = circleCentreYOriginal + winnerRadius + circleRadius; circle4 = new Circle(circleCentreX, circleCentreY, circleRadius); } else if ( i == 4 ) { circleCentreX = circleCentreXOriginal - winnerRadius - circleRadius; circleCentreY = circleCentreYOriginal; circle5 = new Circle(circleCentreX, circleCentreY, circleRadius); } else if ( i == 5 ) { circleCentreX = circleCentreXOriginal; circleCentreY = circleCentreYOriginal; circle6 = new Circle(circleCentreX, circleCentreY, circleRadius); } else if ( i == 6 ) { circleCentreX = circleCentreXOriginal; circleCentreY = circleCentreYOriginal; circle7 = new Circle(circleCentreX, circleCentreY, circleRadius); } } for (int i = 0; i < candidateCount; i++) { Candidate thisCandidate; thisCandidate = (Candidate)thisRiding.candidates.get(i); fill(thisCandidate.getPartyColor()); if ( i == 2 ) { circleA = new Circle(circle1.x, circle1.y, circle1.r + circle3.r); circleB = new Circle(circle2.x, circle2.y, circle2.r + circle3.r); intersectionCoordinates = intersectCircles(circleA, circleB); circle3.setCentre(intersectionCoordinates[2], intersectionCoordinates[3]); ellipse(circle3.x, circle3.y, circle3.r*2, circle3.r*2); } else if ( i == 3 ) { circleA = new Circle(circle1.x, circle1.y, circle1.r + circle4.r); circleB = new Circle(circle3.x, circle3.y, circle3.r + circle4.r); intersectionCoordinates = intersectCircles(circleA, circleB); circle4.setCentre(intersectionCoordinates[2], intersectionCoordinates[3]); ellipse(circle4.x, circle4.y, circle4.r*2, circle4.r*2); } else if ( i == 4 ) { circleA = new Circle(circle1.x, circle1.y, circle1.r + circle5.r); circleB = new Circle(circle4.x, circle4.y, circle4.r + circle5.r); intersectionCoordinates = intersectCircles(circleA, circleB); circle5.setCentre(intersectionCoordinates[2], intersectionCoordinates[3]); ellipse(circle5.x, circle5.y, circle5.r*2, circle5.r*2); } else if ( i == 5 ) { circleA = new Circle(circle1.x, circle1.y, circle1.r + circle6.r); circleB = new Circle(circle5.x, circle5.y, circle5.r + circle6.r); intersectionCoordinates = intersectCircles(circleA, circleB); circle6.setCentre(intersectionCoordinates[2], intersectionCoordinates[3]); ellipse(circle6.x, circle6.y, circle6.r*2, circle6.r*2); } else if ( i == 6 ) { circleA = new Circle(circle1.x, circle1.y, circle1.r + circle7.r); circleB = new Circle(circle6.x, circle6.y, circle6.r + circle7.r); intersectionCoordinates = intersectCircles(circleA, circleB); circle7.setCentre(intersectionCoordinates[2], intersectionCoordinates[3]); ellipse(circle7.x, circle7.y, circle7.r*2, circle7.r*2); } } // draw a scaled, black ring for all the losing votes in this riding votes = thisRiding.getLosingVotes(); circleArea = votes / dividerFactor; circleRadius = sqrt(circleArea / PI); thisRiding.setLoserBlobRadius((int)circleRadius); noFill(); strokeWeight(2); stroke(0,177); ellipse(circleCentreXOriginal,circleCentreYOriginal,circleRadius*2,circleRadius*2); noStroke(); // shift down for the next riding // translate(0,100); // figure out of there's more space in this column if ( (yOffset + ySpacing + yMargin + 100) < height ) { yOffset = yOffset + ySpacing; } else { yOffset = 0; xOffset = xOffset + xSpacing; } resetMatrix(); scale(scaleFactor); // shift the next chart into the calculated position in the grid translate(xOffset+xMargin, yOffset+yMargin); } } class Circle { float x, y, r, r2; Circle( float px, float py, float pr ) { x = px; y = py; r = pr; r2 = r*r; } void setCentre(float _x, float _y) { this.x = _x; this.y = _y; } } float[] intersectCircles( Circle cA, Circle cB ) { float dx = cA.x - cB.x; float dy = cA.y - cB.y; float d2 = dx*dx + dy*dy; float d = sqrt( d2 ); float[] intersectionCoords = new float[4]; if ( d>cA.r+cB.r || d 1) { spos = spos + (newspos-spos)/loose; } } int constrain(int val, int minv, int maxv) { return min(max(val, minv), maxv); } boolean over() { if(mouseX > xpos && mouseX < xpos+swidth && mouseY > ypos && mouseY < ypos+sheight) { return true; } else { return false; } } void draw() { strokeWeight(1); stroke(0); fill(222); rect(xpos, ypos, swidth, sheight); if(over || scrolling) { fill(153, 102, 0); } else { fill(102, 102, 102); } rect(spos, ypos, sheight, sheight); } float getPos() { // convert spos to be values between // 0 and the total width of the scrollbar return spos * ratio; } float getPosRatio() { // NOTE: this hard coding of the .9 is completely mad, but it works when I don't quite understand the scrollbar/image relationship return (spos / swidth) * 0.9; } } class DetailPopup { int xPos, yPos; int xClosePos, yClosePos; int closeWidth, closeHeight; int xOffset, yOffset; PImage windowBackground; Riding ridingToDetail; boolean showing; public DetailPopup() { windowBackground = loadImage("data/popup_background_wide.png"); xClosePos = 383; yClosePos = 14; closeWidth = 22; closeHeight = 22; } void draw() { if ( showing ) { // show the screen PNG file, to the selected riding chart appears highlighted image(highlightScreen, ridingToDetail.chartCentreX - viewportWidth - viewportWidth/2 + 22 - xOffset, ridingToDetail.chartCentreY - viewportHeight - viewportHeight/2 + 38 - yOffset); // show the popup background png image(windowBackground, xPos - xOffset, yPos - yOffset); // add riding name to the popup fill(0); textFont(largeFontBold, 15); int textYOffset = 30; text(ridingToDetail.ridingName, xPos - xOffset + 20, yPos - yOffset + textYOffset); textYOffset = textYOffset + 10; textFont(largeFont, 15); for (int i = 0; i < ridingToDetail.candidates.size(); i++) { Candidate thisCandidate = (Candidate)ridingToDetail.candidates.get(i); textYOffset = textYOffset + 21; fill(thisCandidate.getPartyColor()); noStroke(); ellipse(xPos - xOffset + 24, yPos - yOffset + textYOffset - 6, 14, 14); fill(0); text(thisCandidate.party + " " + insertThousandsSeparators(thisCandidate.votesObtained) + " [" + thisCandidate.percentageVotesObtained + "%]", xPos - xOffset + 35, yPos - yOffset + textYOffset); } textYOffset = textYOffset + 35; fill(ridingToDetail.getWinner().getPartyColor()); noStroke(); ellipse(xPos - xOffset + 24, yPos - yOffset + textYOffset - 5, 14, 14); fill(0); text("votes for the winning candidate " + insertThousandsSeparators(ridingToDetail.getWinningVotes()) + " [" + nf(ridingToDetail.getWinningPercentage(),2,1) + "%]", xPos - xOffset + 35, yPos - yOffset + textYOffset); textYOffset = textYOffset + 21; noFill(); stroke(0); strokeWeight(2); ellipse(xPos - xOffset + 24, yPos - yOffset + textYOffset - 5, 12, 12); fill(0); text("votes for other candidates " + insertThousandsSeparators(ridingToDetail.getLosingVotes()) + " [" + nf(ridingToDetail.getLosingPercentage(),2,1) + "%]", xPos - xOffset + 35, yPos - yOffset + textYOffset); } } void setOffset(int _xOffset, int _yOffset) { xOffset = _xOffset; yOffset = _yOffset; } // mouse clicked void clicked(int _mouseX, int _mouseY) { // test to see whether click was on the close button if ( _mouseX >= (xPos - xOffset + xClosePos) && _mouseX <= (xPos - xOffset + xClosePos + closeWidth) ) { if ( _mouseY >= (yPos + yClosePos) && _mouseY <= (yPos + yClosePos + closeHeight) ) { this.hide(); return; } } // if it wasn't a close click then lets see if it was a click outside the popup on another riding's chart if ( _mouseX < xPos || _mouseX > xPos + windowBackground.width || _mouseY < yPos || _mouseY > yPos + windowBackground.height ) { detailPopup.show(lastHoveredRiding); } } void show(Riding _ridingToDetail) { ridingToDetail = _ridingToDetail; // set the x position of the popup window based on where in the window it falls if ( (ridingToDetail.chartCentreX - xOffset) < viewportWidth/3 ) { xPos = ridingToDetail.chartCentreX - 60; } else if ( (ridingToDetail.chartCentreX - xOffset) > viewportWidth/3 && (ridingToDetail.chartCentreX - xOffset) < 2*(viewportWidth/3) ) { xPos = ridingToDetail.chartCentreX - (windowBackground.width/2); } else if ( (ridingToDetail.chartCentreX - xOffset) > 2*(viewportWidth/3) ) { xPos = ridingToDetail.chartCentreX - windowBackground.width + 70; } // set the y position of the popup window based on where in the window it falls if (ridingToDetail.chartCentreY < height/2) { yPos = ridingToDetail.chartCentreY + 70; } else { yPos = ridingToDetail.chartCentreY - 15 - windowBackground.height; } showing = true; } void hide() { ridingToDetail = null; showing = false; } } // utility function to replace substrings String replace(String str, String pattern, String replace) { int s = 0; int e = 0; StringBuffer result = new StringBuffer(); while ((e = str.indexOf(pattern, s)) >= 0) { result.append(str.substring(s, e)); result.append(replace); s = e+pattern.length(); } result.append(str.substring(s)); return result.toString(); } // function only works for number of a certain size, not what you'd call robust String insertThousandsSeparators(int number) { String pattern = "###,###,###"; DecimalFormat myFormatter = new DecimalFormat(pattern); String output = myFormatter.format(number); return output; }