Skip to main content

World Cup 2022 Part 2

· 5 min read
Simon Porritt

We've been adding new views to our World Cup visualization site over the last few days.


Disclaimer: We didn't actually build this view with the Toolkit! It's not the sort of UI that requires the visual connectivity that the Toolkit provides, but it's a useful view on the tournament, so we'll just include a screenshot here and skip the technical discussion:

Tournament view#

We had this idea back in 2018 but were not able to execute on it because of limitations of the Toolkit's original Hierarchical layout. Now that we have the Hierarchy layout, though, we're able to do it.

This is a view of the entire tournament, from the group stage through to the finals, arranged as a hierarchy. For each group we show the individual matches, as first discussed on our original post 4 years ago (and which you can also see on our site now):

Technical details#

What's cool about this view is that we have mixed up groups with their own internal layouts alongside nodes representing the finals, and the Toolkit allows us to do this declaratively. This is what the view looks like:

{    nodes:{        default:{            template:'<div class="match-node">' +                    '   <div class="team d-flex align-items-center mb-1">' +                    '       <div class="me-2">' +                    '           <img class="flag" src="{{#teamAFlag}}" />' +                    '       </div>' +                    '       <div class="name">{{#teamAName}}</div>' +                    '       <div class="score" data-penalties="{{#teamAPenalties}}">{{#teamAScore}}</div>' +                    '   </div>' +                    '   <div class="team d-flex align-items-center">' +                    '       <div class="me-2">' +                    '           <img class="flag" src="{{#teamBFlag}}" />' +                    '       </div>' +                    '       <div class="name">{{#teamBName}}</div>' +                    '       <div class="score" data-penalties="{{#teamBPenalties}}">{{#teamBScore}}</div>' +                    '   </div>' +                    '</div>'        },        groupMember:{            template:'<div class="group-node" title="{{name}}" rank="{{rank}}">' +                    '   <img class="flag" src="/assets/images/flags/1x1/{{code}}.svg" alt="{{name}} Flag"/>' +                    '</div>'        }    },    groups:{        default:{            template:'<div class="group-vis p-3">' +                    '   <span>{{id}}</span>' +                    '   <div data-jtk-group-content="true"></div>' +                    '</div>',            layout:{                type:CircularLayout.type            }        }    },    edges:{        groupMatch:{            overlays:[                { type:LabelOverlay.type, options:{ label:"{{scoreA}}", location:0.3, cssClass:"p-1"  }},                { type: LabelOverlay.type, options: { label:"{{scoreB}}", location:0.7, cssClass:"p-1"  }}            ],            connector:StraightConnector.type,            endpoint: BlankEndpoint.type,            anchor: AnchorLocations.Center        },        groupWinner:{            overlays:[                { type:LabelOverlay.type, options:{ label:"1st", location:0.75, cssClass:"p-2" }}            ]        },        groupRunnerUp:{            overlays:[                { type:LabelOverlay.type, options:{ label:"2nd", location:0.75, cssClass:"p-2" }}            ]        }    }}
  • We see definitions for two types of nodes - default and groupMember, each of which is mapped to its own template.

  • default nodes are the nodes in the finals, consisting of the team flags and names, plus the score (and penalties, if the match was a draw). The template for this node type makes use of Template Macros to extract values from the backing data for each node.

  • groupMember nodes consist of the team's flag only.

  • Groups are rendered with a separate template, and are assigned a Circular layout, which will be applied to the child members of the group.

  • There are 3 edge types - groupMatch, used inside groups, which shows the score for the match; groupWinner, which is the edge from a group to the round of 16 denoting the group winner, and groupRunnerUp, which is the edge from a group to the round of 16 denoting the group runner up.

  • Edges in the finals section are implicitly of type default and their appearance will be defined either by the Toolkit's defaults, or any default values specified in the render params (see next section)

Render params#
{    layout:{        type:HierarchyLayout.type,        options:{            invert:true,            axis:'vertical',            padding:{                x:100,                y:100            },            gatherUnattachedRoots:true        }    },    templateMacros:{        teamAName:(data:Record<string, any>) => data.teams[0].name,        teamBName:(data:Record<string, any>) => data.teams[1].name,        teamAId:(data:Record<string, any>) => data.teams[0].id,        teamAPenalties:(data:any) => data.penalties[0],        teamAScore:(data:any) => score(0, data),        teamBId:(data:Record<string, any>) => data.teams[1].id,        teamBPenalties:(data:any) => data.penalties[1],        teamBScore:(data:any) => score(1, data),        teamAFlag:(data:any) => `/assets/images/flags/1x1/${data.teams[0].id}.svg`,        teamBFlag:(data:any) => `/assets/images/flags/1x1/${data.teams[1].id}.svg`    },    view,    defaults:{        connector:StateMachineConnector.type,        endpoint: BlankEndpoint.type,        anchors:[ AnchorLocations.ContinuousLeft, AnchorLocations.ContinuousRight]    },    zoomToFit: true,    elementsDraggable:false}

These parameters define the appearance and behaviour of the Surface element.

  • We use a Hierarchy layout, with invert set to true. This means the layout draws itself in reverse order, ie. from root to leaves, and internally we actually import the dataset such that the final and the third place playoffs are root nodes.

  • The layout is drawn such that the elements in the layers are stacked vertically.

  • We use a padding of 100 pixels between elements in each axis.

  • We gather unattached roots next to attached roots. In this case, the effect is to pull the third place playoff element up to sit next to the final match element. If this were not set, the third place playoff would be positioned further down the page, such that a line projecting from it in the X axis would not intersect any of the child elements of the final match.

  • We provide default values for edge appearance, endpoints and anchors (where the connections join the elements)

  • The surface zooms to fit all the content at load time via the zoomToFit parameter

  • Elements are marked not draggable. By default, elements in a Surface are draggable.

What's next?#

If I'm totally honest, I think this visualisation is cooler on paper (and in terms of how easy it is to execute with the Toolkit) than it is to look at, largely because the dataset is too big to be easily seen as a whole (unless, perhaps, you have a really large monitor).

We've got a few ideas for alternate ways to show the entire tournament at a glance, so watch this space.

As always, we'd love to hear your thoughts and suggestions. If you'd like to get in touch, head over to the contact page on our site.