Initial commit.

master
Ben Burlingham 8 years ago
commit 205a42e051
  1. 2
      .gitignore
  2. 1
      README.md
  3. BIN
      css/.DS_Store
  4. 6
      css/index.scss
  5. 9
      css/reset.scss
  6. 148
      index.html
  7. 269
      js/index.js
  8. 15
      package.json
  9. BIN
      res/.DS_Store
  10. 57
      res/sort.svg
  11. 38
      webpack.config.js

2
.gitignore vendored

@ -0,0 +1,2 @@
npm_modules
.DS_Store

@ -0,0 +1 @@
# Dust

BIN
css/.DS_Store vendored

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"
}

BIN
res/.DS_Store vendored

Binary file not shown.

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
width="32"
height="32"
viewBox="0 0 32.000001 32.000001"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="sort.svg"><metadata
id="metadata3425"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs3423" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1439"
inkscape:window-height="848"
id="namedview3421"
showgrid="false"
inkscape:zoom="11.313708"
inkscape:cx="20.100295"
inkscape:cy="16.702877"
inkscape:window-x="2"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="g3415" /><g
id="g3415"
transform="matrix(0.05396372,0,0,0.05826317,-1.4326e-7,-1.2747386)"><g
id="g4315"
transform="matrix(1.1072187,0,0,0.99999937,-12.959153,-2.2752505)"><path
sodipodi:nodetypes="ccc"
inkscape:connector-curvature="0"
id="rect4248"
d="m 547.27246,499.0629 -173.15325,28.27856 -30.6874,-161.194"
style="fill:#000000;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
inkscape:connector-curvature="0"
id="path4279"
d="M 437.90432,126.42218 A 259.43357,240.28901 0 0 0 72.406017,156.03942 259.43357,240.28901 0 0 0 104.38298,494.56631 l 47.64592,-52.59203 a 185.30968,171.635 0 0 1 -22.84069,-241.80491 185.30968,171.635 0 0 1 261.07022,-21.15519 185.30968,171.635 0 0 1 22.84069,241.80491 l 56.78217,44.12996 A 259.43357,240.28901 0 0 0 437.90432,126.42218 Z"
style="fill:#000000;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /></g></g></svg>

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…
Cancel
Save