From 205a42e051961b3534590dee2a8779e14e06eeda Mon Sep 17 00:00:00 2001 From: Ben Burlingham Date: Sat, 25 Mar 2017 07:29:31 -0700 Subject: [PATCH] Initial commit. --- .gitignore | 2 + README.md | 1 + css/.DS_Store | Bin 0 -> 6148 bytes css/index.scss | 6 ++ css/reset.scss | 9 ++ index.html | 148 +++++++++++++++++++++++++ js/index.js | 269 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 15 +++ res/.DS_Store | Bin 0 -> 6148 bytes res/sort.svg | 57 ++++++++++ webpack.config.js | 38 +++++++ 11 files changed, 545 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 css/.DS_Store create mode 100644 css/index.scss create mode 100644 css/reset.scss create mode 100644 index.html create mode 100644 js/index.js create mode 100644 package.json create mode 100644 res/.DS_Store create mode 100644 res/sort.svg create mode 100644 webpack.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..766ad91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +npm_modules +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..03343e6 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Dust diff --git a/css/.DS_Store b/css/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 + + + + + + + + + + + + +

World Cup Matches

+

Chord diagram for all World Cup matches from 1930 to 2014

+
+ +

Project Goal

+ +
+ Explore D3's chord diagrams using World Cup match data. Use Haskell to build JSON parsers. +

+ The visualization should invite interaction to create and answer questions such as: +
    +
  • How many times has Iran competed in a World Cup?
  • +
  • Which teams played the in final in 1986?
  • +
  • Which team scored the most goals in any World Cup?
  • +
  • Why does the 1950 World Cup have 6 final games (and what is the Maracanazo)?
  • +
+
+ +

+ Data source: OpenFootball. + Transform and reduce scripts are built in Haskell, publically available in the source code. +

+ +

+ 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. +

+ +
+ +
+
+
+
+
+
+ +
+ +
+
+ + + + +` diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..a2d90b5 --- /dev/null +++ b/js/index.js @@ -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); diff --git a/package.json b/package.json new file mode 100644 index 0000000..9c36968 --- /dev/null +++ b/package.json @@ -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" +} diff --git a/res/.DS_Store b/res/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 + + +image/svg+xml \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..623c486 --- /dev/null +++ b/webpack.config.js @@ -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 + }) + ] +};