commit
205a42e051
11 changed files with 545 additions and 0 deletions
@ -0,0 +1,2 @@ |
||||
npm_modules |
||||
.DS_Store |
Binary file not shown.
@ -0,0 +1,6 @@ |
||||
.visualization { |
||||
height: 700px; |
||||
margin: 10px auto; |
||||
position: relative; |
||||
width: 1100px; |
||||
} |
@ -0,0 +1,9 @@ |
||||
* { |
||||
box-sizing: border-box; |
||||
margin: 0; |
||||
padding: 0; |
||||
} |
||||
|
||||
body { |
||||
font-family: sans-serif; |
||||
} |
@ -0,0 +1,148 @@ |
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<script src="http://d3js.org/d3.v4.0.0-alpha.50.min.js"></script> |
||||
<script src="http://d3js.org/d3-chord.v0.0.min.js"></script> |
||||
<script src='js/bundle.js'></script> |
||||
<link rel="stylesheet" href="css/style.css"> |
||||
<link rel="stylesheet" href="css/flags.min.css"> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<h1>World Cup Matches</h1> |
||||
<h2>Chord diagram for all World Cup matches from 1930 to 2014</h2> |
||||
<hr> |
||||
|
||||
<h3>Project Goal</h3> |
||||
|
||||
<blockquote> |
||||
Explore D3's chord diagrams using World Cup match data. Use Haskell to build JSON parsers. |
||||
<br><br> |
||||
The visualization should invite interaction to create and answer questions such as: |
||||
<ul> |
||||
<li>How many times has Iran competed in a World Cup?</li> |
||||
<li>Which teams played the in final in 1986?</li> |
||||
<li>Which team scored the most goals in any World Cup?</li> |
||||
<li>Why does the 1950 World Cup have 6 final games (and what is the Maracanazo)?</li> |
||||
</ul> |
||||
</blockquote> |
||||
|
||||
<p> |
||||
Data source: <a href="http://openfootball.github.io/">OpenFootball</a>. |
||||
Transform and reduce scripts are built in Haskell, publically available in <a href="http://gogs.benburlingham.com/ben.burlingham/d3-worldcup">the source code</a>. |
||||
</p> |
||||
|
||||
<p> |
||||
This visualization is created using D3.js. Note that null-null relationships are not displayed on chord diagrams. As a result, matches with a score of 0-0 are not shown. |
||||
</p> |
||||
|
||||
<hr> |
||||
|
||||
<div class="visualization"> |
||||
<div class="tourney"></div> |
||||
<div class="events"></div> |
||||
<div class="sort"></div> |
||||
<div class="schemes"></div> |
||||
<div class="rounds"></div> |
||||
|
||||
<div class="diagram"> |
||||
<svg width="700" height="700"></svg> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- <div class="notes"> |
||||
1930: OK |
||||
1934: Patched. |
||||
OK Italy Spain replay 1-0! |
||||
1938: |
||||
OK Switzerland Germany 1-1 replay 4-2 |
||||
OK Cuba Romania 3-3 replay 3-2 |
||||
OK Brazil Czech 1-1 replay 2-1 |
||||
1950: OK |
||||
1954: |
||||
OK Germany Turkey 4-1 playoff 7-2 |
||||
OK Switzerland Italy 2-1 playoff 4-1 |
||||
OK Germany Hungary 3-8 final 3-2 |
||||
1958: |
||||
Sweden Wales 0-0 |
||||
Brazil England 0-0 |
||||
OK Northern Ireland Czech 1-0 playoff 2-1 |
||||
OK Wales Hungary 1-1 replay 2-1 |
||||
OK Russia England 1-0 replay 2-2 |
||||
1962: |
||||
Germany Italy 0-0 |
||||
Brazil Czech Republic 0-0 |
||||
Hungary Argentina 0-0 |
||||
England Bulgaria 0-0 |
||||
1966: |
||||
England Uruguay 0-0 |
||||
Mexico Uruguay 0-0 |
||||
Argentina Germany 0-0 |
||||
1970: |
||||
Mexico Russia 0-0 |
||||
Uruguay Italy 0-0 |
||||
Israel Italy 0-0 |
||||
1974: |
||||
Australia Chile 0-0 |
||||
Brazil Yugoslavia 0-0 |
||||
Scotland Brazil 0-0 |
||||
Sweden Bulgaria 0-0 |
||||
1978: |
||||
Germany Poland 0-0 |
||||
Germany Tunisia 0-0 |
||||
Brazil Spain 0-0 |
||||
Netherlands Peru 0-0 |
||||
Italy Germany 0-0 |
||||
Argentina Brazil 0-0 |
||||
1982: |
||||
Italy Poland 0-0 |
||||
Peru Cameroon 0-0 |
||||
Poland Cameroon 0-0 |
||||
Serbia Northern Ireland 0-0 |
||||
Russia Poland 0-0 |
||||
Germany England 0-0 |
||||
Spain England 0-0 |
||||
1986: |
||||
Scotland Uruguay 0-0 |
||||
Morocco Poland 0-0 |
||||
1990: |
||||
Uruguay Spain 0-0 |
||||
England Netherlands 0-0 |
||||
Ireland Egypt 0-0 |
||||
1994: |
||||
South Korea Bolivia 0-0 |
||||
Ireland Norway 0-0 |
||||
Brazil Sweden replay |
||||
1998: |
||||
Paraguay Bulgaria 0-0 |
||||
Spain Paraguay 0-0 |
||||
Netherlands Belgium 0-0 |
||||
2002: |
||||
France Uruguay 0-0 |
||||
Nigeria England 0-0 |
||||
OK Brazil Turkey replay 1-0 |
||||
2006: |
||||
Trinidad Tobago Sweden 0-0 |
||||
Netherlands Argentina 0-0 |
||||
Mexico Angola 0-0 |
||||
Japan Croatia 0-0 |
||||
France Switzerland 0-0 |
||||
2010: |
||||
Uruguay France 0-0 |
||||
England Algeria 0-0 |
||||
Paraguay New Zealand 0-0 |
||||
Ivory Coast Portugal 0-0 |
||||
Portugal Brazil 0-0 |
||||
Switzerland Honduras 0-0 |
||||
2014: |
||||
Brazil Mexico 0-0 |
||||
Japan Greece 0-0 |
||||
Costa Rica England 0-0 |
||||
Ecuador France 0-0 |
||||
Iran-Nigeria 0-0 |
||||
</div> --> |
||||
</body> |
||||
</html> |
||||
` |
@ -0,0 +1,269 @@ |
||||
import Matrices from './matrices'; |
||||
import Diagram from './diagram'; |
||||
import UI from './ui'; |
||||
import Hotfixes from './hotfixes'; |
||||
|
||||
require('../css/reset.scss'); |
||||
require('../css/index.scss'); |
||||
require('../css/events.scss'); |
||||
require('../css/tourney.scss'); |
||||
require('../css/diagram.scss'); |
||||
require('../css/sort.scss'); |
||||
require('../css/schemes.scss'); |
||||
require('../css/rounds.scss'); |
||||
|
||||
const main = { |
||||
changeEvent: (e) => { |
||||
const target = UI.findRoot(e.target); |
||||
main.setState({ eventKey: target.getAttribute(UI.DATA.EVENT) }); |
||||
main.updateUI(); |
||||
}, |
||||
|
||||
changeSort: (e) => { |
||||
const target = UI.findRoot(e.target); |
||||
main.setState({ sort: target.getAttribute(UI.DATA.SORT) }); |
||||
main.updateUI(); |
||||
}, |
||||
|
||||
changeScheme: (e) => { |
||||
const target = UI.findRoot(e.target); |
||||
main.setState({ scheme: target.getAttribute(UI.DATA.SCHEME) }); |
||||
main.updateUI(); |
||||
}, |
||||
|
||||
changeRound: (e) => { |
||||
const target = UI.findRoot(e.target); |
||||
const r = target.getAttribute(UI.DATA.ROUND) |
||||
|
||||
const state = main.getState(); |
||||
const roundsToShow = state.rounds ? state.rounds.split(',') : []; |
||||
const i = roundsToShow.indexOf(r); |
||||
(i === -1) ? roundsToShow.push(r) : roundsToShow.splice(i, 1); |
||||
|
||||
main.setState({ rounds: roundsToShow }); |
||||
main.updateUI(); |
||||
}, |
||||
|
||||
// DEPRECATED? 161120
|
||||
// getRounds: (eventKey) => {
|
||||
// const rounds = {};
|
||||
//
|
||||
// main.json.tourneys[eventKey].games.forEach(game => {
|
||||
// const name = UI.getRoundName(main.json.rounds[game.rId]);
|
||||
// if (rounds[name] === undefined) {
|
||||
// rounds[name] = [];
|
||||
// }
|
||||
//
|
||||
// rounds[name].push({
|
||||
// id: game.rId,
|
||||
// name: name,
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// return rounds;
|
||||
// },
|
||||
|
||||
getDuplicates: (eventKey) => { |
||||
const games = {}; |
||||
|
||||
main.json.tourneys[eventKey].games.forEach(game => { |
||||
const teams = [game.t1, game.t2].sort(); |
||||
const addr = `${teams[0]}-${teams[1]}`; |
||||
|
||||
if (games[addr] === undefined) { |
||||
games[addr] = []; |
||||
} |
||||
|
||||
games[addr].push(game); |
||||
}); |
||||
|
||||
return Object.keys(games).reduce((acc, k) => { |
||||
if (games[k].length > 1) { |
||||
acc.push(games[k]); |
||||
} |
||||
|
||||
return acc; |
||||
}, []); |
||||
}, |
||||
|
||||
generateUI: () => { |
||||
const state = main.getState(); |
||||
|
||||
UI.buildTourneyPane(); |
||||
UI.buildEventsPane(main.changeEvent); |
||||
UI.buildSortPane(main.changeSort); |
||||
UI.buildSchemePane(main.changeScheme); |
||||
UI.buildRoundsPane(main.changeRound); |
||||
}, |
||||
|
||||
updateUI: () => { |
||||
const state = main.getState(); |
||||
const eventKey = state.eventKey; |
||||
|
||||
const matrix = Matrices.buildMatrix(main.json, eventKey); |
||||
|
||||
const duplicates = main.getDuplicates(eventKey); |
||||
|
||||
const tmp = Diagram.buildChords({ |
||||
data: main.json, |
||||
sort: state.sort, |
||||
eventKey, |
||||
matrix, |
||||
SORT_TYPES: UI.SORT_TYPES, |
||||
}); |
||||
|
||||
const getScore = (game) => { |
||||
let s1 = game.s1; |
||||
let s2 = game.s2; |
||||
|
||||
if (game.sp1 !== null) { |
||||
s1 = game.sp1; |
||||
} else if (game.se1 !== null) { |
||||
s1 = game.se1; |
||||
} |
||||
|
||||
if (game.sp2 !== null) { |
||||
s2 = game.sp2; |
||||
} else if (game.se2 !== null) { |
||||
s2 = game.se2; |
||||
} |
||||
|
||||
return { s1, s2 } |
||||
}; |
||||
|
||||
const chords = tmp.reduce((acc, d) => { |
||||
const tSrc = main.json.tourneys[eventKey].teams[d.source.index].tId; |
||||
const tTgt = main.json.tourneys[eventKey].teams[d.target.index].tId; |
||||
|
||||
d.game = main.json.tourneys[eventKey].games |
||||
.find(g => { |
||||
return (g.t1 === tSrc && g.t2 === tTgt) || |
||||
(g.t1 === tTgt && g.t2 === tSrc) |
||||
}); |
||||
|
||||
duplicates.forEach(games => { |
||||
if (games[0] === d.game || games[1] === d.game) { |
||||
const gameNew = (games[0] === d.game ? games[1] : games[0]); |
||||
const sourceNew = Object.assign({}, d.source); |
||||
const targetNew = Object.assign({}, d.target); |
||||
|
||||
const sourceAngle = d.source.endAngle - d.source.startAngle; |
||||
const targetAngle = d.target.endAngle - d.target.startAngle; |
||||
|
||||
const { s1: s1g0, s2: s2g0 } = getScore(games[0]); |
||||
const { s1: s1g1, s2: s2g1 } = getScore(games[1]); |
||||
const totals = { src: 0, tgt: 0 }; |
||||
const offset = { src: 0, tgt: 0 } |
||||
|
||||
totals.src += (games[0].t1 === tSrc ? s1g0 : s2g0); |
||||
totals.tgt += (games[0].t1 === tTgt ? s1g0 : s2g0); |
||||
|
||||
totals.src += (games[1].t1 === tSrc ? s1g1 : s2g1); |
||||
totals.tgt += (games[1].t1 === tTgt ? s1g1 : s2g1); |
||||
|
||||
offset.src = (games[0].t1 === tSrc ? s1g0 : s2g0); |
||||
offset.tgt = (games[0].t1 === tTgt ? (totals.tgt - s1g0) : (totals.tgt - s2g0)); |
||||
|
||||
sourceNew.startAngle = d.source.startAngle + sourceAngle * (offset.src / totals.src); |
||||
d.source.endAngle = d.source.startAngle + sourceAngle * (offset.src / totals.src); |
||||
|
||||
targetNew.endAngle = d.target.startAngle + targetAngle * (offset.tgt / totals.tgt); |
||||
d.target.startAngle = d.target.startAngle + targetAngle * (offset.tgt / totals.tgt); |
||||
|
||||
acc.push({ source: sourceNew, target: targetNew, game: gameNew }); |
||||
} |
||||
}); |
||||
|
||||
acc.push(d); |
||||
|
||||
return acc; |
||||
}, []); |
||||
|
||||
chords.groups = tmp.groups; |
||||
|
||||
Diagram.clear(); |
||||
|
||||
const color = Diagram.buildColorScheme({ |
||||
scheme: parseInt(state.scheme), |
||||
len: main.json.tourneys[state.eventKey].teams.length, |
||||
}); |
||||
|
||||
const container = Diagram.buildContainer(chords); |
||||
Diagram.buildArcs({ container, color, eventKey, data: main.json }); |
||||
Diagram.buildRibbons({ container, color, chords, eventKey, data: main.json }); |
||||
|
||||
UI.updateTourneyPane(state.eventKey); |
||||
UI.updateEventsPane(state.eventKey); |
||||
UI.updateSortPane(state.sort); |
||||
UI.updateSchemePane(state.scheme); |
||||
UI.updateRoundsPane(state.rounds.split(','), main.json.rounds); |
||||
}, |
||||
|
||||
fetch: (url) => new Promise((resolve, reject) => { |
||||
const listener = ({ target: req }) => { |
||||
req.status === 200 ? resolve(req.responseText) : reject("busted"); |
||||
}; |
||||
|
||||
const req = new XMLHttpRequest(); |
||||
req.addEventListener('load', listener); |
||||
req.open('GET', url); |
||||
req.send(); |
||||
}), |
||||
|
||||
initJSON: (strData) => { |
||||
main.json = JSON.parse(strData); |
||||
}, |
||||
|
||||
patchErrors: () => { |
||||
main.json.tourneys['1934'] = Hotfixes.patch1934(main.json.tourneys['1934']); |
||||
}, |
||||
|
||||
getState: () => { |
||||
const params = window.location.href.split('?')[1]; |
||||
|
||||
if (!params) { |
||||
return {}; |
||||
} |
||||
|
||||
return params.split('&').reduce((acc, v) => { |
||||
const tmp = v.split('='); |
||||
acc[tmp[0]] = tmp[1]; |
||||
return acc; |
||||
}, {}); |
||||
}, |
||||
|
||||
initState: () => { |
||||
const state = main.getState(); |
||||
|
||||
state.eventKey = state.eventKey || "2014"; |
||||
state.sort = state.sort || null; |
||||
state.scheme = state.scheme || Math.ceil(Math.random() * 4); |
||||
state.rounds = state.rounds || Object.values(UI.ROUND_TYPES); |
||||
|
||||
main.setState(state); |
||||
}, |
||||
|
||||
setState: (next) => { |
||||
const state = main.getState(); |
||||
const url = window.location.href.split('?')[0]; |
||||
|
||||
state.eventKey = next.eventKey || state.eventKey; |
||||
state.rounds = next.rounds || state.rounds; |
||||
state.scheme = next.scheme || state.scheme; |
||||
state.sort = next.sort || state.sort || null; |
||||
|
||||
const params = []; |
||||
for (let key in state) { |
||||
params.push(`${key}=${state[key]}`); |
||||
} |
||||
|
||||
history.pushState(null, null, `${url}?${params.join('&')}`); |
||||
}, |
||||
} |
||||
|
||||
main.fetch('worldcup.json') |
||||
.then(main.initJSON) |
||||
.then(main.patchErrors) |
||||
.then(main.initState) |
||||
.then(main.generateUI) |
||||
.then(main.updateUI); |
@ -0,0 +1,15 @@ |
||||
{ |
||||
"name": "dust", |
||||
"version": "1.0.0", |
||||
"description": "Particle management using streams.", |
||||
"main": "webpack.config.js", |
||||
"scripts": { |
||||
"test": "echo \"Error: no test specified\" && exit 1" |
||||
}, |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "http://gogs.benburlingham.com/ben.burlingham/dust.git" |
||||
}, |
||||
"author": "Ben Burlingham", |
||||
"license": "ISC" |
||||
} |
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
@ -0,0 +1,38 @@ |
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin'); |
||||
|
||||
module.exports = { |
||||
entry: './js/index.js', |
||||
|
||||
module: { |
||||
loaders: [ |
||||
{ |
||||
test: /\.js$/, |
||||
exclude: __dirname + '/node_modules', |
||||
loader: 'babel-loader', |
||||
query: { |
||||
presets: ['es2015'] |
||||
} |
||||
}, |
||||
{ |
||||
test: /\.(scss|css)$/, |
||||
include: __dirname + '/css', |
||||
loader: ExtractTextPlugin.extract('css!sass?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'), |
||||
}, |
||||
{ |
||||
test : /\.svg$/, |
||||
loader : 'file-loader?name=../[path][name].[ext]' |
||||
} |
||||
] |
||||
}, |
||||
|
||||
output: { |
||||
path: __dirname, |
||||
filename: './js/bundle.js' |
||||
}, |
||||
|
||||
plugins: [ |
||||
new ExtractTextPlugin('./css/style.css', { |
||||
allChunks: true |
||||
}) |
||||
] |
||||
}; |
Loading…
Reference in new issue