Community Edition 4.x Beta Release Community Edition 4.x Beta Release
20 Jun 2020
World Cup 2018, Part 2 World Cup 2018, Part 2
26 Jun 2018
World Cup 2018, Part 1 World Cup 2018, Part 1
25 Jun 2018
Community Edition 2.2.2 Release Community Edition 2.2.2 Release
20 Oct 2016
Connecting SVG Shapes (Raphael, Highcharts etc) (update) Connecting SVG Shapes (Raphael, Highcharts etc) (update)
20 Oct 2016
Writing a Layout Decorator Writing a Layout Decorator
25 Jul 2015
Toolkit Edition 1.0.0 Toolkit Edition 1.0.0
23 Jul 2015
Community Edition 1.7.6 Community Edition 1.7.6
23 Jul 2015
Connecting SVG Shapes (Raphael, Highcharts etc) Connecting SVG Shapes (Raphael, Highcharts etc)
22 Jul 2015
The beforeDrag Interceptor The beforeDrag Interceptor
15 Jan 2015
Integrating Jekyll and YUIDoc Integrating Jekyll and YUIDoc
28 Jan 2014
Dragging Multiple Elements Dragging Multiple Elements
17 Jan 2014
Custom Connectors Custom Connectors
22 Dec 2013

World Cup 2018, Part 1

It's world cup time again and I've been looking for a good overview of how it's all progressing in the group stages. Being a
computer programmer I of course spent a small amount of time looking for one done by someone else, and then decided to just
do it myself. With the trusty jsPlumb Toolkit at my disposal I figure it'll be a doddle.

Let's start with the result (this is from the morning of June 25th):

A
B
C
D
E
F
G
H

A visualisation like this gives us a good overview of how each group is progressing. We can see at a glance which matches are yet to be played, and the result of each match that has already been played (at the time of writing there are edges missing from each of these: the group stages are not yet finished). You can also see how the group rankings are coming along: the team with the green border is in first place, the amber border is second, and the teams with the red borders are coming in 3rd and 4th.

This graphic is much easier to parse than the tabular format this information is usually presented in, and satisfies my urge to respect the concept of the "data-ink ratio", as discussed in one of my favourite books of all time, The Visual Display of Quantitative Information. If you haven't read this book I highly recommend it. When I was searching for that link to include, though, I noticed one of the suggestions was "visual display of quantitative information pdf". Please don't steal it. That wouldn't be very cool.

THE DATA

Teams, groups and matches are stored in a JSON object that is loaded as a script. A rough outline:

worldCupData = {
  "teams":{
    "ru":{"code":"ru", "name":"Russia"},
    "uy":{"code":"uy","name":"Uruguay"},
    "eg":{"code":"eg","name":"Egypt"},
    
    ...
  },
  "groups":[
      { "id":"A", "teams":[ "ru", "uy", "eg", "sa" ], "rank":[]},
      { "id":"B", "teams":[ "es", "pt", "ir", "ma"], "rank":[]},
      
      ...
  ],
  "matches":{
      "group":[
          {"id":"1","teams":["ru","sa"],"date":0, "score":[5,0]},
          {"id":"2","teams":["eg","uy"],"date":0, "score":[0,1]},
          {"id":"3","teams":["ir","ma"],"date":0, "score":[1,0]},
          ...
      },    
      "roundof16":[
        { "id":"33", "participants":["C1", "D2"], "score":[], "date":0 },
        { "id":"34", "participants":["A1", "B2"], "score":[], "date":0 },
        { "id":"35", "participants":["B1", "A2"], "score":[], "date":0 },
        ...
      ],      
      "quarters":[
        { "id":"41", "matches":["33", "34"], "score":[], "date":0 },
        { "id":"42", "matches":["35", "36"], "score":[], "date":0 },
        ...
      ],
      "semis":[
        { "id":"45", "matches":["41", "42"], "score":[], "date":0 },
        { "id":"46", "matches":["43", "44"], "score":[], "date":0 }
      ],
      "thirdplace":{
        "score":[], "date":0, "id":"47"
      },
      "final":{
        "score":[], "date":0, "id":"48"
      }
   }
}     

There are of course a million ways to model a dataset. I chose this. Teams are modelled first; each team has a name and
a country code. We then list out the groups, with their ID and list of teams, plus a rank array which the code populates
and is therefore not strictly necessary in the json.

We split the matches into the various phases of the World Cup - first the group matches, then the knockout round of 16,
followed by the quarter finals, the semi finals, and then the third place playoff and the final. Each match has this form:

{ "id":"33", "participants":["C1", "D2"], "score":[], "date":0 }

This example is from the knockout round. participants identifies which teams from the group stage will play in this
match - in this case, the 1st placed team from group C and the 2nd placed team from group D.

For the quarters onwards we slightly change the syntax:

{ "id":"41", "matches":["33", "34"], "score":[], "date":0 },

ie. we use matches instead of participants to identify who will be playing - the winners of the matches with the given
IDs.

Note that for the later stages of the tournament we have not recorded the teams in each match; we populate these dynamically.

Note also the date field, which always has a value of 0 at the moment. This is a placeholder; a future post in this
series may populate this field and then do some tricks with it.

THE CODE

We render each group via this loop:

worldCupData.groups.forEach(renderGroup);

The renderGroup function consists of three parts - first we call processGroup to populate its current state. processGroup looks like this:

function processGroup(group) {

    var nodes = [], edges = [];
    
    for (var i = 0; i < group.teams.length; i++) {
        var teamA = getTeam(group.teams[i]);
        nodes.push(teamA);
    
        for (var j = 0; j < group.teams.length; j++) {
            var teamB = getTeam(group.teams[j]);
            if (teamA.code < teamB.code) {
                var score = getGroupMatchScore(teamA, teamB);
                if (score != null) {
                    edges.push({
                        source: teamA.code,
                        target: teamB.code,
                        data: {scoreA: "" + score[0], scoreB: "" + score[1]}
                    });
    
                    // increment goals
                    teamA.goals += score[0];
                    teamB.goals += score[1];
                    // calculate winner/loser
                    if (score[0] === score[1]) {
                        teamA.draws++;
                        teamB.draws++;
                    } else {
                        teamA[score[0] > score[1] ? "wins" : "losses"]++;
                        teamB[score[0] > score[1] ? "losses" : "wins"]++;
                    }
                }
            }
        }
    }
    
    nodes.sort(function (a, b) {
        if (a.wins > b.wins) {
            return -1;
        } else if (b.wins > a.wins) {
            return 1;
        } else {
            if (a.goals > b.goals) {
                return -1;
            } else {
                return 1;
            }
        }
    });
    
    nodes.forEach(function (n, i) {
        n.rank = i;
        group.rank[i] = n;
    });
    
    return { nodes:nodes, edges:edges };
    }

getTeam looks up a team by its country code and returns an object containing the team's name, code, and count of wins,
draws, losses and goals. Here we add each team from the group as a node, and then for each match that exists
for the given team, we add that match as an edge, populating and adjusting the count of wins, losses, draws and goals
for each team in the match. Note the data member of the edge contains the score; we will use that in the
renderer.

Lastly, we sort the nodes, which is our list of teams, by their number of wins, or if they have the same number of wins
as some other team, by the number of goals they have scored (football experts: is this accurate?). We then inject the calculated
rank into each team object.

Now we've got a suitable dataset we instantiate a Toolkit instance and render:

    
var tk = jsPlumbToolkit.newInstance({ idFunction:function(d) { return d.code; } });
tk.render({
    container:document.getElementById(group.id),
    layout:{
        type:"Circular"
    },
    view:{
        nodes:{
            default:{
                template:"team"
            }
        },
        edges:{
            default:{
                overlays:[
                    [ "Label", { label:"${scoreA}", location:0.3 }],
                    [ "Label", { label:"${scoreB}", location:0.7 }]
                ]
            }
        }
    },
    jsPlumb:{
        Connector:"Straight",
        Endpoint:"Blank",
        Anchor:"Center"
    },
    templates:{
        "team":"<div class=\"group-node\" title=\"${name}\" rank=\"${rank}\"><img class=\"flag\" src=\"/path/to/flags/4x3/${code}.svg\" alt=\"${name} Flag\"/></div>"
    },
    enableWheelZoom:false,
    enablePan:false,
    zoomToFit:true
});

Points to note:

  • we use the Circular layout to place the teams. For a demonstration of this, see this page.
  • there are two overlays declared for each edge; they are of type Label and their label value extracts data from the edge's backing node.
  • team nodes are rendered as an image with an SVG for the country. These were sourced from this excellent project. Tak, @lipis and friends!
  • we disable pan and zoom on each group visualisation, via enableWheelZoom:false and enablePan:false.
  • via zoomToFit:true, we instruct the renderer to pan and zoom after data load so that the entire dataset is visible and centered in the viewport
  • we provide the templates to use inline, via the templates parameter. We could also provision them as elements in the DOM, but these posts are passed through a markdown renderer and it was causing me grief when compiling the page so I switched to inline template provision.
  • our template renders each teams rank as an attribute with the name rank. We use CSS to colour code our nodes according to the value of the rank attribute.

Lastly, we load our dataset:

tk.load({
    data:groupData
});

THE CSS

These are the styles used in this page:

    
    /* the container for all the groups visualisations */
    #groups {
        display:flex;
        flex-wrap:wrap;
    }
    
    /* a single group visualisation */
    .group-vis {
        border:2px solid #CCC;
        flex-basis:300px;
        width:300px;
        height:300px;
        overflow:hidden !important;
        margin:10px;
        position:relative;
    }   
    
    /* the group name element in each group */
    .group-vis span {
        position:absolute;
        top:0;
        bottom:0;
        left:0;
        right:0;
        display:flex;
        justify-content:center;
        align-items:center;
        font-size:170px;
        color:#ededf1;
    }
    
    /* a team node in a group. by default has a red border */ 
    .group-node {
        border-radius: 50%;
        border:3px solid red;
    }
    
    /* the team ranked 1st in the group has a green border. `rank` was extracted from the team data by the template. */  
    .group-node[rank="0"] {
        border-color:green;
    }
    
    /* the team ranked 2nd has an orange border */
    .group-node[rank="1"] {
        border-color:orange;
    }
    
    /* flag image for each team */
    .group-node img {
        width:40px;
        height:40px;
        border-radius: 50%;
        border:2px solid #CCC;
    }
    
    /* assigned to all toolkit nodes. we set z-index so they appear above the edges representing each match */
    .jtk-node {
        z-index:1;
    }
    
    /* assigned to all toolkit overlays. we use overlays for the score of each match */
    .jtk-overlay {
        width:15px;
        height:15px;
        background-color:white;
        display:flex;
        justify-content:center;
        align-items:center;
        font-size:12px;
    }
    
    /* these are "nudge" buttons for panning, visible by default. we hide them in this visualisation.
    .jtk-surface-pan {
        display:none;   
    }

It's all pretty standard CSS. One thing to note is the way we use the rank member of each team's data to render a custom
attribute, which we then style via css.

WHAT NEXT?

This is part one of what we intend to be a few posts, in which we'll progressively build up our visualisation to give us
different views of what's going on at the World Cup. Stay tuned!