Loads of people watch Eurovision for the outrageous costumes, hit-and-miss music, and general titivation. I appreciate all that stuff too, of course, but what Iām really living for on the night of the Eurovision grand final is the voting.
I donāt think Iām actually ever more excited than when they announce the results; even more so with the new and absolutely brutal way theyāve done them in recent years.
So when I was deciding how I could bring some added fun to my Eurovision house party this year, there was only ever one choice: more voting! Many people would have settled for a paper scorecard downloaded from the Beeb ā but not me. No, I wanted a digital scoreboard with realtime voting for my guests. And thatās what I built. Hereās a quick canter through how I shoved it together in only a few hours.
What I built
Described simply, I built a webpage with a grid layout. In each cell of the grid was a country name and a score ā which was pulled in from a Google Sheet roughly every 30 seconds.
It looks like this:
To āvoteā, you filled in a Google Form and gave each country a score on the usual 1 to 12 Eurovision voting scale. The Google Sheet then aggregated the votes and gave an average score for each song.
Starting with a boilerplate
As with most of my digital projects nowadays, I find it quickest to use Jekyll. It was entirely unnecessary to use Jekyll to do this ā but it does have the benefit of building remotely on Github Pages without a faff, and it wallpapers over some of the significant gaps in my javascript skills.
Iām not going to go into how to put a Jekyll site together ā the official documentation is simple enough that most people can follow it. I wonāt go over the very rudimentary HTML and CSS Iāve used to create the scoreboard either ā if you know how to code at a fairly rudimentary level, youāll be able to replicate it.
Instead, Iāll skip to the interesting bit ā how I got the data out of Google Sheets.
Setting up Google Sheets
Start with your Google Form
I started by creating a Google Form. On the form, I included 3 questions:
- which country the person was voting for
- how many points they were awarding
- who they were
Guests awarded their points by submitting the form at the end each performance1.
Link your Google Form to a Google Sheet
Google Form created, I next linked this to a Google Sheet. This means every time someone makes a form submission, it saves their response to a spreadsheet.
Itās almost too easy to do: you just click a button on the top of the form! In my case, I needed to create a new Sheets file, but you could also link it to an existing spreadsheet if you had one youād already been working in.
Calculate the scores
The next task is to aggregate the scores.
Each form submission is added to the workbook as a row in a sheet (Iāll call this the āResponses sheetā for brevity). So to get the aggregated scores, in a new sheet I:
- listed the names of the countries in the final in Column A
- used
SUMIF()
to check if the country in Column A matched the response on the āResponses sheetā - used
COUNTIF()
to see how many times the name of the country in Column A appeared in the āResponses sheetā - used
AVERAGE()
to average theSUMIF()
andCOUNTIF()
- rounded the average to the nearest integer by using
ROUND()
- if the rounded number was either
9
or11
, I rounded up to10
or12
respectively; because you canāt get 9 or 11 points in Eurovision
All this resulted in a final score for each entry.
Publish the Google Sheet
In order to retrieve the data for the scoreboard, I published the Google Sheet, which makes it available publicly to anyone with the link2.
To publish the sheet, go to File > Publish to the web
, and click Publish
. I also ticked the Automatically republish when changes are made
box.
Get your Google Sheets identifiers
One final thing I needed was the identifier for the Google Sheet from the URL.
Google Sheets URLs (at the time of writing) are structured like this:
https://docs.google.com/spreadsheets/d/YOUR-SPREADSHEET-ID/edit#gid=WORKSHEET-ID
You need to know the YOUR-SPREADSHEET-ID
ā which will be a long random alphanumeric string ā in order to point the code in the next section to the right place.
Getting the data out of Google Sheets
Before you go further, I should say that Iām very much a javascript novice. I pieced this together from various StackOverflow threads, so whilst I can tell you what I did, I canāt necessarily explain to you why it works!
Thereās probably a much simpler way of writing this code ā if there is, please do let me know and amend the code on Github.
Load the data
The first thing I needed was a function that could load the data. I created a function loadScript()
which accepts two arguments ā the url
of the Google Sheet and a callback
to trigger once itās loaded.
function loadScript(url, callback)
{
// Adding the script tag to the head as suggested before
var head = document.head;
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
// Then bind the event to the callback function.
// There are several events for cross browser compatibility.
script.onreadystatechange = callback;
script.onload = callback;
// Fire the loading
head.appendChild(script);
}
The url
argument is constructed from the YOUR-SPREADSHEET-ID
you hunted down earlier.
Whereas the URL for the document in a browser will load the Google Sheets app, I needed the data in a raw format. Google offers a JSONP API for this purpose. The URL we need for our scriptās url
argument is in this format:
https://spreadsheets.google.com/feeds/cells/YOUR-SPREADSHEET-ID/1/public/basic?alt=json-in-script&callback=YOUR-CALLBACK
You replace YOUR-SPREADSHEET-ID
with the ID you kept hold of earlier.
In order to manipulate the data you need to include a callback too ā thatās where YOUR-CALLBACK
comes in. In my case, I called it onDataLoaded
; which weāll come back to.
That gave me this:
loadScript("https://spreadsheets.google.com/feeds/cells/1tbhEoWuhBg5H5l61VMsqy-qywp4LCzj5ZsoD9dSvftw/1/public/basic?alt=json-in-script&callback=onDataLoaded")
This script will now call the data, inject the script into the page header
, make a callback onDataLoaded
and ā if specified ā make a second callback to continue with the rest of the operations I need.
Do something with the data
I included a callback in the URL, onDataLoaded
. This function would take the content of specific cells, save the data as variables, and then place this data inside a div
in the HTML.
I mentioned I used Jekyll; this is where Jekyll comes in handy. Instead of manually writing out each var
and popping it into itās respective div
, I created a .csv
file in the siteās _data
folder called data.csv
. In this file was a list of all the countries in the final.
In the javascript file, I used some Liquid markup to iterate through each row of data.csv
, to convert any country names into usable variables, and then replace the HTML. What would have been more than 50 lines of code to write then became 4 lines3.
var onDataLoaded = (data) => {
{%- for entry in site.data.data -%}
var {{ entry.Name | remove: " " }} = data.feed.entry.find((entry) => entry.title.$t == '{{ entry.CellReference }}').content.$t
document.getElementById('entry-{{ entry.Name | remove: " " }}').innerHTML = {{ entry.Name | remove: " " }}
{%- endfor -%}
}
Check the data regularly
So I can now get the data, and I can place the data I need in the places I want; but it only loads once. The feed will update automatically (because I set it to publish changes automatically at the start), but the page wonāt know that so the scores only update when the page refreshes.
To get around this, I created a getNewData()
function. This function triggers the loadScript()
function every n seconds thanks to a setInterval()
.
function getNewData() {
setInterval(
function(){
loadScript("https://spreadsheets.google.com/feeds/cells/YOUR-SPREADSHEET-ID/1/public/basic?alt=json-in-script&callback=onDataLoaded")
// Output to the console that the data has been checked,
// so you know it's working even if there are no changes.
console.log("Updated data");
},
30000);
}
Load the script, and keep checking it
This new getNewData()
function becomes part of our initial loadScript()
function call, when the page has finished loading.
I wrapped the loadScript()
in a window.onload
, calling the URL and the getNewData
function, like this:
window.onload = function() {
loadScript("https://spreadsheets.google.com/feeds/cells/YOUR-SPREADSHEET-ID/1/public/basic?alt=json-in-script&callback=onDataLoaded", getNewData)
};
This means that, after the page has loaded, the script will download the data from the Google Sheet, manipulate the data, and then set a timer to trigger the same command again via getNewData
.
Final javascript code
Put all that together, and you get this:
window.onload = function() {
loadScript("https://spreadsheets.google.com/feeds/cells/YOUR-SPREADSHEET-ID/1/public/basic?alt=json-in-script&callback=onDataLoaded", getNewData)
};
function getNewData() {
setInterval(
function(){
loadScript("https://spreadsheets.google.com/feeds/cells/YOUR-SPREADSHEET-ID/1/public/basic?alt=json-in-script&callback=onDataLoaded")
console.log("Updated data");
},
30000);
}
function loadScript(url, callback)
{
// Adding the script tag to the head as suggested before
var head = document.head;
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
// Then bind the event to the callback function.
// There are several events for cross browser compatibility.
script.onreadystatechange = callback;
script.onload = callback;
// Fire the loading
head.appendChild(script);
}
var onDataLoaded = (data) => {
{%- for entry in site.data.data -%}
var {{ entry.Name | remove: " " }} = data.feed.entry.find((entry) => entry.title.$t == '{{ entry.CellReference }}').content.$t
document.getElementById('entry-{{ entry.Name | remove: " " }}').innerHTML = {{ entry.Name | remove: " " }}
{%- endfor -%}
}
What Iād do differently next time
I did this in a rush; I had the idea at 10am on the morning of the final and had finished it just after lunch time. That means there are a lot of rough edges, including:
- Lots of unnecessary code: I copied an existing Jekyll project folder over and started hacking away at it, so thereās a lot of code I donāt need thatās left behind from that. If I do this again next year, Iāll tidy that all up.
- It works best on TVs: I knew I was going to display this on a 1080p resolution display, so I built it specifically for that use case. It goes quite badly wrong, quite quickly on any other screen size. An objective for next time would be to make it responsive.
- Jekyll wasnāt really needed: I could have built this without Jekyll, but itās become a bit of a crutch recently. I should probably build it without Jekyll!
Thereās also some stuff that would have been cool to add, like:
- Re-ordering the grid automatically: at the moment, all scores are ordered by the order they appear in the
data.csv
file. It would be great to have them re-order to be in rank order. - Calculate proper scores, after voting ends: in the current version, itās possible to have more than one act with 12 points. It would be cool to have the scoreboard use the unrounded figures to attribute scores properly at the end, Eurovision style.
Get the code
All the code for this project is available on Github, if you want to role your own.
Footnotes
If we were strictly mirroring the way Eurovision voting really works, each person would get to award their points once ā theyād give 12 points to their favourite country, 10 points to their second favourite, and so on ā but that seemed to defeat the point of having a live scoreboard. So instead, I allowed guests to give 0, 1, 2, 3, 4, 5, 6, 7, 8, 10 or 12 points to each country. ↩︎
It is technically possible to make this work without making the Google Sheet publicly visible, but that would involve all sorts of tricky authentication that was too much of a hassle. ↩︎
I could have achieved the same thing in javascript, but I was in a rush and didnāt want to brush up on javascript arrays! ↩︎