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