├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── assets └── data │ ├── basketball │ └── 2015-2016 │ │ └── nba-western.csv │ ├── chgk │ └── 2015-2016 │ │ └── student-european-championship.csv │ ├── football │ ├── 2015-2016 │ │ └── english-premier-league.csv │ └── 2016-2017 │ │ ├── english-premier-league.json │ │ └── portugese-primeira-liga.json │ ├── formula-one │ └── 2016 │ │ ├── constructors.csv │ │ └── drivers.csv │ └── requests │ └── 2016 │ └── lebedian-rpfl16-17.csv ├── development.md ├── dist ├── assets │ └── data │ │ ├── basketball │ │ └── 2015-2016 │ │ │ └── nba-western.csv │ │ ├── chgk │ │ └── 2015-2016 │ │ │ └── student-european-championship.csv │ │ ├── football │ │ ├── 2015-2016 │ │ │ └── english-premier-league.csv │ │ └── 2016-2017 │ │ │ └── english-premier-league.json │ │ ├── formula-one │ │ └── 2016 │ │ │ └── drivers.csv │ │ └── requests │ │ └── 2016 │ │ └── lebedian-rpfl16-17.csv ├── index.html ├── replay-table.css ├── replay-table.js └── replay-table.min.js ├── index.html ├── package.json ├── src ├── calculate │ ├── calculate.js │ ├── calculations.js │ ├── calculators │ │ ├── add-meta.js │ │ ├── enrich.js │ │ ├── index.js │ │ ├── position.js │ │ └── sort.js │ ├── config.js │ └── helpers │ │ ├── get-compare-function.js │ │ └── sort-round-results.js ├── configure │ ├── configs │ │ └── index.js │ ├── configure.js │ ├── extensions │ │ └── index.js │ ├── helpers │ │ ├── add-id.js │ │ ├── extend-configs.js │ │ ├── get-empty-config.js │ │ ├── get-preset-config.js │ │ └── map-param-to-module.js │ ├── initialize.js │ ├── parametrize.js │ └── presets │ │ ├── chgk.js │ │ ├── f1.js │ │ ├── index.js │ │ ├── matches.js │ │ └── win-loss.js ├── extract │ ├── config.js │ ├── extract.js │ ├── extractors │ │ ├── csv.js │ │ ├── index.js │ │ └── json.js │ └── helpers │ │ └── guess-extractor.js ├── helpers │ ├── crash.js │ ├── data │ │ ├── get-item-results.js │ │ └── get-items.js │ ├── general │ │ ├── flip-object.js │ │ ├── from-camel-case.js │ │ ├── get-file-extension.js │ │ ├── is-between.js │ │ ├── is-string.js │ │ ├── json-parse.js │ │ ├── number-to-change.js │ │ ├── to-camel-case.js │ │ ├── to-css.js │ │ └── transpose.js │ ├── parsing │ │ └── parse-object.js │ ├── validation │ │ ├── validate-array.js │ │ └── validate-object.js │ └── warn.js ├── magic.js ├── replay-table.css ├── replay-table.js ├── transform │ ├── config.js │ ├── configs │ │ ├── index.js │ │ ├── list-of-matches.js │ │ └── points-table.js │ ├── helpers │ │ └── match │ │ │ ├── flip.js │ │ │ └── getOutcome.js │ ├── post-transformers │ │ ├── collapse-to-rounds.js │ │ ├── filter-items.js │ │ ├── index.js │ │ └── insert-start-round.js │ ├── transform.js │ └── transformers │ │ ├── index.js │ │ ├── list-of-matches.js │ │ └── points-table.js └── visualize │ ├── cell.js │ ├── config.js │ ├── configs │ ├── classic.js │ ├── index.js │ └── sparklines.js │ ├── controls │ ├── index.js │ ├── next.js │ ├── play.js │ ├── previous.js │ └── slider.js │ ├── helpers │ ├── adjust-durations.js │ ├── format-position.js │ ├── get-rows-ys.js │ └── sparklines │ │ ├── get-spark-classes.js │ │ └── get-spark-color.js │ ├── skeleton.js │ ├── visualize.js │ └── visualizers │ ├── classic.js │ ├── index.js │ └── sparklines.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { presets: ['es2015'] } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | assets/csv/ 3 | .idea/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Visual management software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Replay Table 2 | ========= 3 | 4 | A library fo visualizing sport season results with interactive standings: 5 | 6 | ![sparklines-demo](https://antoniokov.com/replay/assets/images/sparklines_demo.gif) 7 | 8 | ## Live Demos 9 | 10 | * [English Premier League](https://antoniokov.com/replay#replay-english-premier-league-2016-2017) 11 | * [Formula One](https://antoniokov.com/replay#replay-formula-one-drivers-2016) 12 | * [NBA](https://antoniokov.com/replay#replay-nba-western-2015-2016) 13 | 14 | ## Quickstart 15 | 16 | 1. Prepare an [input file](#input) with season results or download one from our [examples](https://antoniokov.com/replay/#examples). 17 | 18 | 2. Put a `div` with `replayTable` class on your page and supply a link to the input file using `data-source` attribute: 19 | 20 | ``` 21 |
23 |
24 | ``` 25 | 3. Include D3.js and Replay Table scripts (70+16 KB gzipped) and the stylesheet. Apply some magic to the body: 26 | 27 | ``` 28 | 29 | ... 30 | 31 | 32 | 33 | 34 | 35 | ... 36 | 37 | 38 | ``` 39 | 40 | 4. Enjoy! 41 | 42 | The library is highly customizable via `data-` attributes. Check out [customization](#customization) for the details. 43 | 44 | Also feel free to embed ready-to-use Replay Tables from our [gallery](https://antoniokov.com/replay/#examples). 45 | 46 | 47 | ## Library 48 | 49 | `npm install -S replay-table` 50 | 51 | The only dependency is [D3.js](d3js.org). D3 is not included with the library so don't forget to plug it up. 52 | 53 | The library consists of 5 modules: configure → extract → transform → calculate → visualize. 54 | Take a look at how we use them in the `magic` function: 55 | 56 | ``` 57 | return Array.from(document.getElementsByClassName('replayTable')) 58 | .map(table => { 59 | const config = replayTable.configure(table.id, table.dataset); //build a config from data- attributes 60 | 61 | return Promise.resolve(replayTable.extract(config.extract)) //fetch the input 62 | .then(raw => { 63 | const transformed = replayTable.transform(raw, config.transform); //transform into predefined format 64 | const calculated = replayTable.calculate(transformed, config.calculate); //calculate wins, goals, etc. 65 | return replayTable.visualize(calculated, config.visualize); //render interactive standings 66 | }) 67 | .catch(error => crash(error)); 68 | }); 69 | ``` 70 | 71 | Sometimes you won't need all the modules: for example, feel free to omit `configure` if you already have a config. 72 | 73 | A Replay Table is returned from the `visualize` module. It has methods like `play()` and `to(roundIndex)` so you can control its behaviour from code. 74 | 75 | 76 | ## Customization 77 | * [Configure](#configure) 78 | * [Presets](#presets) 79 | * [Extract](#extract) 80 | * [Transfrom](#transform) 81 | * [List of Matches](#list-of-matches) 82 | * [Points Table](#points-table) 83 | * [Calculate](#calculate) 84 | * [Visualize](#visualize) 85 | * [Classic](#classic) 86 | * [Sparklines](#sparklines) 87 | 88 | ## Configure 89 | 90 | Makes configs for other modules based on the div `data-` attributes. The output looks like this: 91 | 92 | ``` 93 | extract: { 94 | extractor: csv 95 | }, 96 | transform: { 97 | transformer: 'pointsTable', 98 | changeToOutcome: { 99 | 25: 'win' 100 | }, 101 | insertStartRound: 'Start →' 102 | }, 103 | calculate: { 104 | orderBy: ['points', 'wins'] 105 | }, 106 | visualize: { 107 | columns: ['position', 'item', 'points', 'points.change'], 108 | labels: ['#', 'Driver', 'Points'] 109 | } 110 | ``` 111 | 112 | ### Presets 113 | 114 | To save you some time and cognitive effort we've constructed presets 115 | that you can use via `data-preset` attribute: 116 | [matches](https://github.com/antoniokov/replay-table/blob/master/src/configure/presets/matches.js), 117 | [f1](https://github.com/antoniokov/replay-table/blob/master/src/configure/presets/f1.js), 118 | [winLoss](https://github.com/antoniokov/replay-table/blob/master/src/configure/presets/win-loss.js), 119 | [chgk](https://github.com/antoniokov/replay-table/blob/master/src/configure/presets/chgk.js). 120 | 121 | So this table: 122 | 123 | ``` 124 |
126 | data-transformer="listOfMatches" 127 | data-change-to-outcome="{ 1: 'win', 0: 'loss' }" 128 | data-order-by="winningPercentage,wins" 129 | data-visualizer="classic" 130 | data-columns="position,item,rounds,wins,losses,winningPercentage,outcome,match" 131 | data-labels="#,Team,G,W,L,Win %" 132 |
133 | ``` 134 | 135 | is identical to this: 136 | 137 | ``` 138 |
140 | data-preset="winLoss" 141 |
142 | ``` 143 | 144 | ## Extract 145 | 146 | Fetches the input file, returns a promise. 147 | 148 | 149 | | Parameter | Attribute | Required | Accepts | Default | Examples | 150 | |-----------|-----------|----------|---------------|---------------|----------| 151 | | source | `data-source` | yes | `string` | `null` | `/assets/data/football/2016-2017/english-premier-league.json` | 152 | | extractor | `data-extractor` | no | [extractor](https://github.com/antoniokov/replay-table/tree/master/src/extract/extractors) | `csv` | `csv`, `json` | 153 | 154 | If extractor is not defined we try to guess it from the file extension. 155 | 156 | ## Transform 157 | 158 | Transforms raw data into the predefined format: 159 | 160 | ``` 161 | [ 162 | { 163 | name: 'round name', 164 | results: { 165 | [ 166 | { 167 | change: 25, 168 | extras: { 169 | item: { 170 | team: 'Mercedes' 171 | } 172 | }, 173 | item: 'Lewis Hamilton', 174 | outcome: 'win' 175 | }, 176 | { 177 | ... 178 | }, 179 | ... 180 | ] 181 | } 182 | }, 183 | { 184 | ... 185 | }, 186 | ... 187 | ] 188 | ``` 189 | 190 | | Parameter | Attribute | Accepts | Parses | Default | Examples | 191 | |-----------|-----------|---------|--------|---------------|----------| 192 | | transformer | `data-transformer` | [transformer](https://github.com/antoniokov/replay-table/tree/master/src/transform/transformers) | | `listOfMatches` | `listOfMatches`, `pointsTable` | 193 | | changeToOutcome | `data-change-to-outcome` | `object` | JSON object | `{ 3: 'win', 1: 'draw', 0: 'loss'}` | `{ 1: 'win', 0: 'loss'}` | 194 | | filterItems | `data-filter-items` | `array of strings` | comma-separated string | `[]` | `['Golden State Warriors', 'San Antonio Spurs', ...]` | 195 | | insertStartRound | `data-insert-start-round` | `string` | | `0` | `Start` | 196 | 197 | 198 | ### List of Matches 199 | 200 | The structure looks like this: 201 | 202 | | Round name | First Item | First Item Score | Second Item | Second Item Score | 203 | |------------|------------|------------------|-------------|-------------------| 204 | | Round | Item | Score | Item | Score | 205 | 206 | List should be sorted by round. 207 | 208 | Here is an example: 209 | 210 | | Match Week | Home | Points | Away | Points | 211 | |------|------|--------|------|--------| 212 | | 1 | Bournemouth | 0 | Aston Villa | 1 | 213 | | 1 | Chelsea | 2 | Swansea | 2 | 214 | | 1 | Everton | 2 | Watford | 2 | 215 | | ... | ... | ... | ... | ... | 216 | 217 | Also works with [football-data.org fixtures](http://api.football-data.org/v1/competitions/426/fixtures). 218 | 219 | 220 | | Parameter | Attribute | Accepts | Default | Examples | 221 | |-----------|-----------|---------|---------------|----------| 222 | | format | `data-format` | `csv` or `football-data.org` | `csv` | `csv`, `football-data.org` | 223 | | locationFirst | `data-location-first` | `home` or `away` | `home` | `home`, `away` | 224 | | collapseToRounds | `data-collapse-to-rounds` | `boolean` | `false` | `true`, `false` | 225 | 226 | Use `collapseToRounds` when you've got dates instead of match weeks: it groups each team's 1st, 2nd, 3rd, ... games. 227 | 228 | ### Points Table 229 | 230 | The structure looks like this: 231 | 232 | | Item name | [1st extra column name] | [2nd extra column name] | [...] | 1st round name | 2nd round name | ... | last round name | 233 | |-----------|-------------------------|-------------------------|-----|----------------|----------------|-----|-----------------| 234 | | item | [1st piece of extra info] | [2nd piece of extra info] | [...] | 1st round points | 2nd round points | ... | last round points | 235 | 236 | The Formula One example ([csv](https://antoniokov.com/replay/assets/data/formula-one/2016/formula-one-drivers.csv)): 237 | 238 | | Driver | Team | Australia | Bahrain | ... | Abu Dhabi | 239 | |------|---|---|---|-----|----| 240 | | Lewis Hamilton | Mercedes | 18 | 15 | ... | 25 | 241 | | Nico Rosberg | Mercedes | 25 | 25 | ... | 18 | 242 | | Daniel Ricciardo | Red Bull | 12 | 12 | ... | 10 | 243 | | ... | ... | .... | ... | ... | ... | 244 | 245 | Watch the [live demo](https://antoniokov.com/replay/#replay-formula-one-drivers-2016). 246 | 247 | | Parameter | Attribute | Accepts | Default | Examples | 248 | |-----------|-----------|---------|---------------|----------| 249 | | extraColumnsNumber | `data-extra-columns-number` | `int` | `0` | `1`, `2` | 250 | 251 | 252 | ## Calculate 253 | 254 | Calculates wins, goals, points, etc. and adds metadata. The output looks like this: 255 | 256 | ``` 257 | { 258 | meta: { 259 | lastRound: 38 260 | }, 261 | results: { 262 | [ 263 | { 264 | meta: { 265 | index: 2, 266 | isLast: false, 267 | items: 20, 268 | name: "2" 269 | }, 270 | results: { 271 | [ 272 | { 273 | change: 3, 274 | draws: { 275 | change: 0, 276 | total: 0 277 | }, 278 | extras: {}, 279 | item: 'Leicester', 280 | losses: { 281 | change: 0, 282 | total: 0 283 | }, 284 | match: { 285 | location: "away", 286 | opponent: "West Ham", 287 | opponentScore: 1, 288 | score: 2 289 | }, 290 | outcome: "win", 291 | points: { 292 | change: 3, 293 | total: 6 294 | }, 295 | position: { 296 | highest: 1, 297 | lowest: 4, 298 | strict: 1 299 | }, 300 | wins: { 301 | change: 1, 302 | total: 2 303 | } 304 | ...//goalsFor, goalsAgainst, goalsDifference, rounds, winningPercentage 305 | }, 306 | .... 307 | ] 308 | } 309 | }, 310 | ... 311 | ] 312 | } 313 | } 314 | ``` 315 | 316 | See the whole list of calculations in the [calculations.js](https://github.com/antoniokov/replay-table/blob/master/src/calculate/calculations.js). 317 | 318 | 319 | | Parameter | Attribute | Accepts | Parses | Default | Examples | 320 | |-----------|-----------|---------|--------|---------------|----------| 321 | | orderBy | `data-order-by` | `array` of calculations | comma-separated string | `['points']` | `['winningPercentage', 'wins']` | 322 | 323 | 324 | ## Visualize 325 | 326 | Renders interactive standings out of calculated data. 327 | 328 | Returns a class instance with useful methods: 329 | * `first()`, `last()`, `next()`, `previous()` and `to(roundIndex)` 330 | * `play()` and `pause()` 331 | * `preview(roundIndex)` and `endPreview()` 332 | * `drillDown(item)` and `endDrillDown()` 333 | 334 | 335 | | Parameter | Attribute | Accepts | Parses | Default | Examples | 336 | |-----------|-----------|---------|--------|---------------|----------| 337 | | visualizer | `data-visualizer` | [visualizer](https://github.com/antoniokov/replay-table/tree/master/src/visualize/visualizers) | | `classic` | `classic`, `sparklines` | 338 | | controls | `data-conrols` | `array` of [controls](https://github.com/antoniokov/replay-table/tree/master/src/visualize/controls) | comma-separated string | `['play', 'previous', 'next', 'slider']` | `['play', 'slider']` | 339 | | startFromRound | `data-start-from-round` | `int` | | `null` | `0`, `15` | 340 | | roundsTotalNumber | `data-rounds-total-number` | `int` | | `null` | `38`, `82` | 341 | | positionWhenTied | `data-position-when-tied` | `int` | | `strict`, `highest`, `range` or `average` | `strict`, `highest` | 342 | | animationSpeed | `data-animation-speed` | `float` | | `1.0` | `0.5`, `2.0` | 343 | 344 | 345 | ### Classic 346 | 347 | ![classic-f1-demo](https://antoniokov.com/replay/assets/images/classic_f1_demo.gif) 348 | 349 | [Formula One](https://antoniokov.com/replay#replay-formula-one-drivers-2016) 350 | 351 | 352 | A simple table with controls on top. Works for any sport and is highly customizable. 353 | 354 | | Parameter | Attribute | Accepts | Parses | Default | Examples | 355 | |-----------|-----------|---------|--------|---------------|----------| 356 | | columns | `data-columns` | `array` of columns | comma-separated string | `['position', 'item', 'points']` | `['position', 'item', 'points', 'outcome', 'points.change]` | 357 | | labels | `data-labels` | `array of strings` | comma-separated string | `['#', 'Team', 'Points']` | `['Position', 'Driver', 'Points']` | 358 | | colors | `data-colors` | `object` | JSON object | `{ 'win': '#ACE680', 'draw': '#B3B3B3', 'loss': '#E68080' }` | `{ 'win': 'green', 'draw': 'gray', 'loss': 'red' }` | 359 | | durations | `data-durations` | `object` | JSON object | `{ move: 750, freeze: 750, outcomes: 200}` | `{ move: 500, freeze: 400, outcomes: 250}` | 360 | 361 | ### Sparklines 362 | 363 | ![sparklines-demo](https://antoniokov.com/replay/assets/images/sparklines_demo.gif) 364 | 365 | [English Premier League](https://antoniokov.com/replay#replay-english-premier-league-2016-2017) 366 | 367 | A powerful interactive visualization for the sports with matches and points. Might be slow on old devices and in Firefox. 368 | 369 | | Parameter | Attribute | Accepts | Parses | Default | Examples | 370 | |-----------|-----------|---------|--------|---------------|----------| 371 | | controls | `data-conrols` | `array` of [controls](https://github.com/antoniokov/replay-table/tree/master/src/visualize/controls) | comma-separated string | `['play']` | `['play', 'previous', 'next']` | 372 | | colors | `data-colors` | `object` | JSON object | `{ 'win': '#21c114', 'draw': '#828282', 'loss': '#e63131' }` | `{ 'win': 'green', 'draw': 'gray', 'loss': 'red' }` | 373 | | sparkColors | `data-spark-colors` | `object` | JSON object | `{ 'win': '#D7E7C1', 'draw': '#F0F0F0', 'loss': '#EFCEBA' }` | `{ 'win': 'green', 'draw': 'gray', 'loss': 'red' }` | 374 | | currentSparkColors | `data-current-spark-colors` | `object` | JSON object | `{ 'win': '#AAD579', 'draw': '#CCCCCC', 'loss': '#E89B77' }` | `{ 'win': 'green', 'draw': 'gray', 'loss': 'red' }` | 375 | | durations | `data-durations` | `object` | JSON object | `{ move: 1000, freeze: 500, pre: 750}` | `{ move: 750, freeze: 750, pre: 375}` | 376 | | pointsLabel | `data-points-label` | `string` | | `points` | `очков` | 377 | | allLabel | `data-all-label` | `string` | | `All` | `Все` | 378 | 379 | ## Contribution 380 | 381 | Please, post your suggestions and bugs via Github issues. PRs are also welcome! 382 | 383 | If you own an API or a database with sports results we'd be happy to collaborate. 384 | 385 | We'd also be happy to work with sport journalists to leverage Replay Table for a better season recap. 386 | 387 | ## Credits 388 | 389 | The library was built using the [orange time](http://www.openwork.org/targetprocess/) at [Targetprocess](https://www.targetprocess.com/) 390 | by Anton Iokov ([@antoniokov](https://github.com/antoniokov)) and Daria Krupenkina ([@dariak](https://github.com/dariak)). 391 | 392 | [Sparklines](#sparklines) prototype was made by Vitali Yanusheuski. 393 | 394 | 395 | ## Contact 396 | 397 | We're open to your ideas and are ready to help with integrating Replay Table into your website. 398 | 399 | Please, write us an email to [anton.iokov@targetprocess.com](mailto:anton.iokov@targetprocess.com) or ping on Twitter at [@antoniokov](https://twitter.com/antoniokov). 400 | -------------------------------------------------------------------------------- /assets/data/chgk/2015-2016/student-european-championship.csv: -------------------------------------------------------------------------------- 1 | Команда,Страна,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75 2 | Wings Gaming,Россия,0,1,0,1,1,0,1,1,0,1,0,0,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,0,1,1,1,1,0,1,1,0,0,0,0,1,0,1,0 3 | Первая сборная,Россия,1,0,0,1,0,0,1,1,1,1,1,1,1,0,1,1,1,1,0,1,1,1,0,0,1,1,1,1,1,0,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,1,0,1,1,0,0,1,1,1,1,1,0,0,1,1,0,1,0,1,0,0,0,1,1,0,0,1,0 4 | Шесть пик,Россия,0,1,0,1,0,0,1,1,1,1,1,1,0,0,0,1,1,0,1,1,1,1,0,0,1,0,1,1,0,0,1,1,0,1,1,1,1,0,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,0,1,1,1,0,0,0,0,1,0,1,1,1,0,1,0 5 | Мискузи,Россия,1,1,0,1,1,0,1,0,0,1,0,1,0,0,1,1,1,1,0,1,1,1,0,1,1,0,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,0,1,0,0,1,0,1,1,0,1,1,0,1,1,0,1,1,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,0 6 | Цветы,Россия,0,1,0,0,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,1,1,1,0,0,1,0,1,1,1,0,1,1,0,0,1,0,1,0,1,1,0,0,1,0,0,0,1,1,0,0,1,0,1,0,0,1,1,1,1,0,1,1,1,1,0,0,0,1,1,1,0,1,0,1,0 7 | Очень изменилась за лето,Беларусь,0,1,0,1,0,0,0,1,1,1,1,1,0,1,0,1,1,0,0,1,0,1,0,1,1,1,0,1,0,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,0,1,0,0,0,1,0,0,0,1,1,1,1,0,0,0,0,1,1,0,1,0,1,0 8 | Рыболюди,Украина,0,1,0,1,0,0,0,0,1,1,1,1,0,1,0,1,1,0,0,1,1,1,0,1,1,0,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,0,1,0,1,1,0,0,1,1,1,1,0,0,1,1,0,0,1,1,1,1,1,0,1,0,0,0,0,0,0,0,1,0 9 | White and nerdy,Россия,1,1,0,1,0,0,1,1,1,1,1,0,0,1,0,1,1,1,0,1,1,1,0,0,1,0,1,1,1,0,1,1,0,0,1,1,0,1,1,1,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,0,0,1,0,1,0,0,1,0,0,0,1,1,0,0,0,0,1,0 10 | Трактор в поле сыр-сыр-сыр,Россия,0,1,0,0,1,1,0,0,1,1,1,1,0,1,1,1,1,1,0,1,0,1,0,1,1,1,0,1,0,0,1,1,0,0,1,1,1,1,0,0,1,0,0,0,0,0,1,1,1,1,1,0,1,1,0,1,1,1,1,0,1,0,0,0,0,0,1,0,0,1,0,1,0,1,0 11 | Корпрусариум,Беларусь,1,1,0,0,1,0,0,0,1,0,0,1,0,0,0,1,1,0,1,1,1,1,0,1,1,0,1,0,1,0,1,1,0,0,1,0,1,1,0,1,1,0,1,1,0,1,1,1,0,0,1,0,1,0,1,0,1,1,0,0,1,1,1,0,0,0,0,1,0,1,1,0,0,1,0 12 | На заре,Беларусь,1,1,0,0,0,0,0,1,0,0,1,1,1,0,0,1,1,0,0,0,0,1,0,1,1,1,0,1,1,0,0,1,0,1,1,0,1,1,0,1,1,0,1,0,0,1,0,0,0,1,1,0,1,0,1,1,1,1,1,0,1,1,1,0,0,0,0,1,1,1,0,0,0,1,0 13 | Имитируем сарказм,Беларусь,0,1,0,1,0,0,1,0,1,1,1,1,1,0,1,1,0,1,0,1,1,1,0,0,0,0,0,1,1,0,1,1,1,0,1,0,0,0,0,0,1,0,1,0,0,0,1,0,1,1,1,0,0,0,1,0,1,0,0,1,1,1,0,1,0,0,0,1,0,1,0,1,0,1,0 14 | Комплексные URумбаи,Беларусь,0,1,0,0,1,0,0,1,0,1,0,1,1,0,0,1,0,0,0,1,1,1,0,1,1,0,0,1,0,0,0,1,1,0,1,1,1,0,0,0,1,0,1,1,1,1,1,0,0,1,1,0,1,0,0,0,1,1,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,1,0 15 | Ультиматум Дорна,Украина,1,1,1,0,1,1,0,0,0,0,0,1,0,0,0,1,0,0,1,0,0,1,0,0,1,1,1,1,1,0,1,0,0,1,1,1,0,1,1,0,1,0,0,0,0,1,0,0,1,1,0,0,1,0,1,1,1,0,0,0,1,1,1,0,0,1,0,1,0,0,0,0,0,1,0 16 | Altavista,Беларусь,0,1,0,1,1,1,0,0,1,1,1,1,1,0,0,1,1,1,0,1,0,1,0,1,0,0,1,1,0,0,0,1,1,0,1,0,0,0,1,0,1,0,0,0,0,1,0,0,0,1,1,0,1,1,1,0,0,1,0,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0 17 | Sortasidra,Россия,1,1,0,0,1,0,1,0,1,0,1,1,0,0,0,1,0,0,0,0,1,1,0,1,0,0,0,1,1,1,0,1,0,0,1,1,1,1,0,0,0,0,0,0,1,1,0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,0,0,0,0,0,0,1,0,0,0,1,0 18 | Колянизация,Украина,0,1,0,1,0,0,1,0,1,0,1,1,1,0,0,1,1,0,0,1,0,1,0,1,0,1,0,1,0,0,1,1,0,0,1,1,0,1,0,0,1,0,0,0,0,1,0,0,1,1,0,0,0,1,1,0,0,0,0,0,1,0,1,0,1,1,0,0,0,0,0,1,0,1,0 19 | Donkey Hot,Украина,0,1,0,1,1,1,1,0,1,0,0,1,0,0,0,1,1,1,0,0,0,1,0,1,0,0,1,1,1,0,1,0,1,0,1,1,1,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,1,0 20 | Сборная Армении,Армения,0,0,0,1,0,0,0,0,1,0,0,1,1,1,0,1,1,0,1,1,0,1,0,1,0,1,0,1,0,0,0,1,1,0,0,1,0,0,0,1,1,0,1,0,0,0,1,0,1,0,0,0,0,1,0,0,0,1,1,1,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0 21 | Британские учёные,Латвия,1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,1,1,0,0,0,0,1,1,0,0,1,0,1,0,1,0,1,1,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,1,0 22 | Восточный Мордор,Украина,0,1,0,0,0,0,0,0,1,0,1,1,1,0,0,1,0,1,0,1,1,1,0,1,1,0,1,1,0,0,0,0,1,0,1,1,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,1,0,0,1,0,0,1,1,0,0,0,0,1,0,0,1,0,1,0,0,0 23 | Холодец безжалостный,Россия,0,1,0,1,0,0,1,0,1,0,0,0,0,0,0,1,1,0,0,1,0,1,0,0,1,0,1,1,1,0,0,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,0,1,0,0,0 24 | Эстонский экспресс,Беларусь,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,0,1,1,0,0,0,0,0,0,0,1,0,1,1,0,1,0,1,1,0,0,0,0,1,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,1,0 25 | Сборная Молдовы,Молдова,0,1,0,1,0,0,0,0,0,0,0,0,1,0,0,1,1,0,1,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0 -------------------------------------------------------------------------------- /assets/data/football/2015-2016/english-premier-league.csv: -------------------------------------------------------------------------------- 1 | Date,HomeTeam,FTHG,AwayTeam,FTAG 2 | 08.08.2015,Bournemouth,0,Aston Villa,1 3 | 08.08.2015,Chelsea,2,Swansea,2 4 | 08.08.2015,Everton,2,Watford,2 5 | 08.08.2015,Leicester,4,Sunderland,2 6 | 08.08.2015,Man United,1,Tottenham,0 7 | 08.08.2015,Norwich,1,Crystal Palace,3 8 | 09.08.2015,Arsenal,0,West Ham,2 9 | 09.08.2015,Newcastle,2,Southampton,2 10 | 09.08.2015,Stoke,0,Liverpool,1 11 | 10.08.2015,West Brom,0,Man City,3 12 | 14.08.2015,Aston Villa,0,Man United,1 13 | 15.08.2015,Southampton,0,Everton,3 14 | 15.08.2015,Sunderland,1,Norwich,3 15 | 15.08.2015,Swansea,2,Newcastle,0 16 | 15.08.2015,Tottenham,2,Stoke,2 17 | 15.08.2015,Watford,0,West Brom,0 18 | 15.08.2015,West Ham,1,Leicester,2 19 | 16.08.2015,Crystal Palace,1,Arsenal,2 20 | 16.08.2015,Man City,3,Chelsea,0 21 | 17.08.2015,Liverpool,1,Bournemouth,0 22 | 22.08.2015,Crystal Palace,2,Aston Villa,1 23 | 22.08.2015,Leicester,1,Tottenham,1 24 | 22.08.2015,Man United,0,Newcastle,0 25 | 22.08.2015,Norwich,1,Stoke,1 26 | 22.08.2015,Sunderland,1,Swansea,1 27 | 22.08.2015,West Ham,3,Bournemouth,4 28 | 23.08.2015,Everton,0,Man City,2 29 | 23.08.2015,Watford,0,Southampton,0 30 | 23.08.2015,West Brom,2,Chelsea,3 31 | 24.08.2015,Arsenal,0,Liverpool,0 32 | 29.08.2015,Aston Villa,2,Sunderland,2 33 | 29.08.2015,Bournemouth,1,Leicester,1 34 | 29.08.2015,Chelsea,1,Crystal Palace,2 35 | 29.08.2015,Liverpool,0,West Ham,3 36 | 29.08.2015,Man City,2,Watford,0 37 | 29.08.2015,Newcastle,0,Arsenal,1 38 | 29.08.2015,Stoke,0,West Brom,1 39 | 29.08.2015,Tottenham,0,Everton,0 40 | 30.08.2015,Southampton,3,Norwich,0 41 | 30.08.2015,Swansea,2,Man United,1 42 | 12.09.2015,Arsenal,2,Stoke,0 43 | 12.09.2015,Crystal Palace,0,Man City,1 44 | 12.09.2015,Everton,3,Chelsea,1 45 | 12.09.2015,Man United,3,Liverpool,1 46 | 12.09.2015,Norwich,3,Bournemouth,1 47 | 12.09.2015,Watford,1,Swansea,0 48 | 12.09.2015,West Brom,0,Southampton,0 49 | 13.09.2015,Leicester,3,Aston Villa,2 50 | 13.09.2015,Sunderland,0,Tottenham,1 51 | 14.09.2015,West Ham,2,Newcastle,0 52 | 19.09.2015,Aston Villa,0,West Brom,1 53 | 19.09.2015,Bournemouth,2,Sunderland,0 54 | 19.09.2015,Chelsea,2,Arsenal,0 55 | 19.09.2015,Man City,1,West Ham,2 56 | 19.09.2015,Newcastle,1,Watford,2 57 | 19.09.2015,Stoke,2,Leicester,2 58 | 19.09.2015,Swansea,0,Everton,0 59 | 20.09.2015,Liverpool,1,Norwich,1 60 | 20.09.2015,Southampton,2,Man United,3 61 | 20.09.2015,Tottenham,1,Crystal Palace,0 62 | 26.09.2015,Leicester,2,Arsenal,5 63 | 26.09.2015,Liverpool,3,Aston Villa,2 64 | 26.09.2015,Man United,3,Sunderland,0 65 | 26.09.2015,Newcastle,2,Chelsea,2 66 | 26.09.2015,Southampton,3,Swansea,1 67 | 26.09.2015,Stoke,2,Bournemouth,1 68 | 26.09.2015,Tottenham,4,Man City,1 69 | 26.09.2015,West Ham,2,Norwich,2 70 | 27.09.2015,Watford,0,Crystal Palace,1 71 | 28.09.2015,West Brom,2,Everton,3 72 | 03.10.2015,Aston Villa,0,Stoke,1 73 | 03.10.2015,Bournemouth,1,Watford,1 74 | 03.10.2015,Chelsea,1,Southampton,3 75 | 03.10.2015,Crystal Palace,2,West Brom,0 76 | 03.10.2015,Man City,6,Newcastle,1 77 | 03.10.2015,Norwich,1,Leicester,2 78 | 03.10.2015,Sunderland,2,West Ham,2 79 | 04.10.2015,Arsenal,3,Man United,0 80 | 04.10.2015,Everton,1,Liverpool,1 81 | 04.10.2015,Swansea,2,Tottenham,2 82 | 17.10.2015,Chelsea,2,Aston Villa,0 83 | 17.10.2015,Crystal Palace,1,West Ham,3 84 | 17.10.2015,Everton,0,Man United,3 85 | 17.10.2015,Man City,5,Bournemouth,1 86 | 17.10.2015,Southampton,2,Leicester,2 87 | 17.10.2015,Tottenham,0,Liverpool,0 88 | 17.10.2015,Watford,0,Arsenal,3 89 | 17.10.2015,West Brom,1,Sunderland,0 90 | 18.10.2015,Newcastle,6,Norwich,2 91 | 19.10.2015,Swansea,0,Stoke,1 92 | 24.10.2015,Arsenal,2,Everton,1 93 | 24.10.2015,Aston Villa,1,Swansea,2 94 | 24.10.2015,Leicester,1,Crystal Palace,0 95 | 24.10.2015,Norwich,0,West Brom,1 96 | 24.10.2015,Stoke,0,Watford,2 97 | 24.10.2015,West Ham,2,Chelsea,1 98 | 25.10.2015,Bournemouth,1,Tottenham,5 99 | 25.10.2015,Liverpool,1,Southampton,1 100 | 25.10.2015,Man United,0,Man City,0 101 | 25.10.2015,Sunderland,3,Newcastle,0 102 | 31.10.2015,Chelsea,1,Liverpool,3 103 | 31.10.2015,Crystal Palace,0,Man United,0 104 | 31.10.2015,Man City,2,Norwich,1 105 | 31.10.2015,Newcastle,0,Stoke,0 106 | 31.10.2015,Swansea,0,Arsenal,3 107 | 31.10.2015,Watford,2,West Ham,0 108 | 31.10.2015,West Brom,2,Leicester,3 109 | 01.11.2015,Everton,6,Sunderland,2 110 | 01.11.2015,Southampton,2,Bournemouth,0 111 | 02.11.2015,Tottenham,3,Aston Villa,1 112 | 07.11.2015,Bournemouth,0,Newcastle,1 113 | 07.11.2015,Leicester,2,Watford,1 114 | 07.11.2015,Man United,2,West Brom,0 115 | 07.11.2015,Norwich,1,Swansea,0 116 | 07.11.2015,Stoke,1,Chelsea,0 117 | 07.11.2015,Sunderland,0,Southampton,1 118 | 07.11.2015,West Ham,1,Everton,1 119 | 08.11.2015,Arsenal,1,Tottenham,1 120 | 08.11.2015,Aston Villa,0,Man City,0 121 | 08.11.2015,Liverpool,1,Crystal Palace,2 122 | 21.11.2015,Chelsea,1,Norwich,0 123 | 21.11.2015,Everton,4,Aston Villa,0 124 | 21.11.2015,Man City,1,Liverpool,4 125 | 21.11.2015,Newcastle,0,Leicester,3 126 | 21.11.2015,Southampton,0,Stoke,1 127 | 21.11.2015,Swansea,2,Bournemouth,2 128 | 21.11.2015,Watford,1,Man United,2 129 | 21.11.2015,West Brom,2,Arsenal,1 130 | 22.11.2015,Tottenham,4,West Ham,1 131 | 23.11.2015,Crystal Palace,0,Sunderland,1 132 | 28.11.2015,Aston Villa,2,Watford,3 133 | 28.11.2015,Bournemouth,3,Everton,3 134 | 28.11.2015,Crystal Palace,5,Newcastle,1 135 | 28.11.2015,Leicester,1,Man United,1 136 | 28.11.2015,Man City,3,Southampton,1 137 | 28.11.2015,Sunderland,2,Stoke,0 138 | 29.11.2015,Liverpool,1,Swansea,0 139 | 29.11.2015,Norwich,1,Arsenal,1 140 | 29.11.2015,Tottenham,0,Chelsea,0 141 | 29.11.2015,West Ham,1,West Brom,1 142 | 05.12.2015,Arsenal,3,Sunderland,1 143 | 05.12.2015,Chelsea,0,Bournemouth,1 144 | 05.12.2015,Man United,0,West Ham,0 145 | 05.12.2015,Southampton,1,Aston Villa,1 146 | 05.12.2015,Stoke,2,Man City,0 147 | 05.12.2015,Swansea,0,Leicester,3 148 | 05.12.2015,Watford,2,Norwich,0 149 | 05.12.2015,West Brom,1,Tottenham,1 150 | 06.12.2015,Newcastle,2,Liverpool,0 151 | 07.12.2015,Everton,1,Crystal Palace,1 152 | 12.12.2015,Bournemouth,2,Man United,1 153 | 12.12.2015,Crystal Palace,1,Southampton,0 154 | 12.12.2015,Man City,2,Swansea,1 155 | 12.12.2015,Norwich,1,Everton,1 156 | 12.12.2015,Sunderland,0,Watford,1 157 | 12.12.2015,West Ham,0,Stoke,0 158 | 13.12.2015,Aston Villa,0,Arsenal,2 159 | 13.12.2015,Liverpool,2,West Brom,2 160 | 13.12.2015,Tottenham,1,Newcastle,2 161 | 14.12.2015,Leicester,2,Chelsea,1 162 | 19.12.2015,Chelsea,3,Sunderland,1 163 | 19.12.2015,Everton,2,Leicester,3 164 | 19.12.2015,Man United,1,Norwich,2 165 | 19.12.2015,Newcastle,1,Aston Villa,1 166 | 19.12.2015,Southampton,0,Tottenham,2 167 | 19.12.2015,Stoke,1,Crystal Palace,2 168 | 19.12.2015,West Brom,1,Bournemouth,2 169 | 20.12.2015,Swansea,0,West Ham,0 170 | 20.12.2015,Watford,3,Liverpool,0 171 | 21.12.2015,Arsenal,2,Man City,1 172 | 26.12.2015,Aston Villa,1,West Ham,1 173 | 26.12.2015,Bournemouth,0,Crystal Palace,0 174 | 26.12.2015,Chelsea,2,Watford,2 175 | 26.12.2015,Liverpool,1,Leicester,0 176 | 26.12.2015,Man City,4,Sunderland,1 177 | 26.12.2015,Newcastle,0,Everton,1 178 | 26.12.2015,Southampton,4,Arsenal,0 179 | 26.12.2015,Stoke,2,Man United,0 180 | 26.12.2015,Swansea,1,West Brom,0 181 | 26.12.2015,Tottenham,3,Norwich,0 182 | 28.12.2015,Arsenal,2,Bournemouth,0 183 | 28.12.2015,Crystal Palace,0,Swansea,0 184 | 28.12.2015,Everton,3,Stoke,4 185 | 28.12.2015,Man United,0,Chelsea,0 186 | 28.12.2015,Norwich,2,Aston Villa,0 187 | 28.12.2015,Watford,1,Tottenham,2 188 | 28.12.2015,West Brom,1,Newcastle,0 189 | 28.12.2015,West Ham,2,Southampton,1 190 | 29.12.2015,Leicester,0,Man City,0 191 | 30.12.2015,Sunderland,0,Liverpool,1 192 | 02.01.2016,Arsenal,1,Newcastle,0 193 | 02.01.2016,Leicester,0,Bournemouth,0 194 | 02.01.2016,Man United,2,Swansea,1 195 | 02.01.2016,Norwich,1,Southampton,0 196 | 02.01.2016,Sunderland,3,Aston Villa,1 197 | 02.01.2016,Watford,1,Man City,2 198 | 02.01.2016,West Brom,2,Stoke,1 199 | 02.01.2016,West Ham,2,Liverpool,0 200 | 03.01.2016,Crystal Palace,0,Chelsea,3 201 | 03.01.2016,Everton,1,Tottenham,1 202 | 12.01.2016,Aston Villa,1,Crystal Palace,0 203 | 12.01.2016,Bournemouth,1,West Ham,3 204 | 12.01.2016,Newcastle,3,Man United,3 205 | 13.01.2016,Chelsea,2,West Brom,2 206 | 13.01.2016,Liverpool,3,Arsenal,3 207 | 13.01.2016,Man City,0,Everton,0 208 | 13.01.2016,Southampton,2,Watford,0 209 | 13.01.2016,Stoke,3,Norwich,1 210 | 13.01.2016,Swansea,2,Sunderland,4 211 | 13.01.2016,Tottenham,0,Leicester,1 212 | 16.01.2016,Aston Villa,1,Leicester,1 213 | 16.01.2016,Bournemouth,3,Norwich,0 214 | 16.01.2016,Chelsea,3,Everton,3 215 | 16.01.2016,Man City,4,Crystal Palace,0 216 | 16.01.2016,Newcastle,2,West Ham,1 217 | 16.01.2016,Southampton,3,West Brom,0 218 | 16.01.2016,Tottenham,4,Sunderland,1 219 | 17.01.2016,Liverpool,0,Man United,1 220 | 17.01.2016,Stoke,0,Arsenal,0 221 | 18.01.2016,Swansea,1,Watford,0 222 | 23.01.2016,Crystal Palace,1,Tottenham,3 223 | 23.01.2016,Leicester,3,Stoke,0 224 | 23.01.2016,Man United,0,Southampton,1 225 | 23.01.2016,Norwich,4,Liverpool,5 226 | 23.01.2016,Sunderland,1,Bournemouth,1 227 | 23.01.2016,Watford,2,Newcastle,1 228 | 23.01.2016,West Brom,0,Aston Villa,0 229 | 23.01.2016,West Ham,2,Man City,2 230 | 24.01.2016,Arsenal,0,Chelsea,1 231 | 24.01.2016,Everton,1,Swansea,2 232 | 02.02.2016,Arsenal,0,Southampton,0 233 | 02.02.2016,Crystal Palace,1,Bournemouth,2 234 | 02.02.2016,Leicester,2,Liverpool,0 235 | 02.02.2016,Man United,3,Stoke,0 236 | 02.02.2016,Norwich,0,Tottenham,3 237 | 02.02.2016,Sunderland,0,Man City,1 238 | 02.02.2016,West Brom,1,Swansea,1 239 | 02.02.2016,West Ham,2,Aston Villa,0 240 | 03.02.2016,Everton,3,Newcastle,0 241 | 03.02.2016,Watford,0,Chelsea,0 242 | 06.02.2016,Aston Villa,2,Norwich,0 243 | 06.02.2016,Liverpool,2,Sunderland,2 244 | 06.02.2016,Man City,1,Leicester,3 245 | 06.02.2016,Newcastle,1,West Brom,0 246 | 06.02.2016,Southampton,1,West Ham,0 247 | 06.02.2016,Stoke,0,Everton,3 248 | 06.02.2016,Swansea,1,Crystal Palace,1 249 | 06.02.2016,Tottenham,1,Watford,0 250 | 07.02.2016,Bournemouth,0,Arsenal,2 251 | 07.02.2016,Chelsea,1,Man United,1 252 | 13.02.2016,Bournemouth,1,Stoke,3 253 | 13.02.2016,Chelsea,5,Newcastle,1 254 | 13.02.2016,Crystal Palace,1,Watford,2 255 | 13.02.2016,Everton,0,West Brom,1 256 | 13.02.2016,Norwich,2,West Ham,2 257 | 13.02.2016,Sunderland,2,Man United,1 258 | 13.02.2016,Swansea,0,Southampton,1 259 | 14.02.2016,Arsenal,2,Leicester,1 260 | 14.02.2016,Aston Villa,0,Liverpool,6 261 | 14.02.2016,Man City,1,Tottenham,2 262 | 27.02.2016,Leicester,1,Norwich,0 263 | 27.02.2016,Southampton,1,Chelsea,2 264 | 27.02.2016,Stoke,2,Aston Villa,1 265 | 27.02.2016,Watford,0,Bournemouth,0 266 | 27.02.2016,West Brom,3,Crystal Palace,2 267 | 27.02.2016,West Ham,1,Sunderland,0 268 | 28.02.2016,Tottenham,2,Swansea,1 269 | 28.02.2016,Man United,3,Arsenal,2 270 | 01.03.2016,Aston Villa,1,Everton,3 271 | 01.03.2016,Bournemouth,2,Southampton,0 272 | 01.03.2016,Leicester,2,West Brom,2 273 | 01.03.2016,Norwich,1,Chelsea,2 274 | 01.03.2016,Sunderland,2,Crystal Palace,2 275 | 02.03.2016,Arsenal,1,Swansea,2 276 | 02.03.2016,Liverpool,3,Man City,0 277 | 02.03.2016,Man United,1,Watford,0 278 | 02.03.2016,Stoke,1,Newcastle,0 279 | 02.03.2016,West Ham,1,Tottenham,0 280 | 05.03.2016,Chelsea,1,Stoke,1 281 | 05.03.2016,Everton,2,West Ham,3 282 | 05.03.2016,Man City,4,Aston Villa,0 283 | 05.03.2016,Newcastle,1,Bournemouth,3 284 | 05.03.2016,Southampton,1,Sunderland,1 285 | 05.03.2016,Swansea,1,Norwich,0 286 | 05.03.2016,Tottenham,2,Arsenal,2 287 | 05.03.2016,Watford,0,Leicester,1 288 | 06.03.2016,Crystal Palace,1,Liverpool,2 289 | 06.03.2016,West Brom,1,Man United,0 290 | 12.03.2016,Bournemouth,3,Swansea,2 291 | 12.03.2016,Norwich,0,Man City,0 292 | 12.03.2016,Stoke,1,Southampton,2 293 | 13.03.2016,Aston Villa,0,Tottenham,2 294 | 14.03.2016,Leicester,1,Newcastle,0 295 | 19.03.2016,Chelsea,2,West Ham,2 296 | 19.03.2016,Crystal Palace,0,Leicester,1 297 | 19.03.2016,Everton,0,Arsenal,2 298 | 19.03.2016,Swansea,1,Aston Villa,0 299 | 19.03.2016,Watford,1,Stoke,2 300 | 19.03.2016,West Brom,0,Norwich,1 301 | 20.03.2016,Man City,0,Man United,1 302 | 20.03.2016,Newcastle,1,Sunderland,1 303 | 20.03.2016,Southampton,3,Liverpool,2 304 | 20.03.2016,Tottenham,3,Bournemouth,0 305 | 02.04.2016,Arsenal,4,Watford,0 306 | 02.04.2016,Aston Villa,0,Chelsea,4 307 | 02.04.2016,Bournemouth,0,Man City,4 308 | 02.04.2016,Liverpool,1,Tottenham,1 309 | 02.04.2016,Norwich,3,Newcastle,2 310 | 02.04.2016,Stoke,2,Swansea,2 311 | 02.04.2016,Sunderland,0,West Brom,0 312 | 02.04.2016,West Ham,2,Crystal Palace,2 313 | 03.04.2016,Leicester,1,Southampton,0 314 | 03.04.2016,Man United,1,Everton,0 315 | 09.04.2016,Aston Villa,1,Bournemouth,2 316 | 09.04.2016,Crystal Palace,1,Norwich,0 317 | 09.04.2016,Man City,2,West Brom,1 318 | 09.04.2016,Southampton,3,Newcastle,1 319 | 09.04.2016,Swansea,1,Chelsea,0 320 | 09.04.2016,Watford,1,Everton,1 321 | 09.04.2016,West Ham,3,Arsenal,3 322 | 10.04.2016,Liverpool,4,Stoke,1 323 | 10.04.2016,Sunderland,0,Leicester,2 324 | 10.04.2016,Tottenham,3,Man United,0 325 | 13.04.2016,Crystal Palace,0,Everton,0 326 | 16.04.2016,Chelsea,0,Man City,3 327 | 16.04.2016,Everton,1,Southampton,1 328 | 16.04.2016,Man United,1,Aston Villa,0 329 | 16.04.2016,Newcastle,3,Swansea,0 330 | 16.04.2016,Norwich,0,Sunderland,3 331 | 16.04.2016,West Brom,0,Watford,1 332 | 17.04.2016,Arsenal,1,Crystal Palace,1 333 | 17.04.2016,Bournemouth,1,Liverpool,2 334 | 17.04.2016,Leicester,2,West Ham,2 335 | 18.04.2016,Stoke,0,Tottenham,4 336 | 19.04.2016,Newcastle,1,Man City,1 337 | 20.04.2016,Liverpool,4,Everton,0 338 | 20.04.2016,Man United,2,Crystal Palace,0 339 | 20.04.2016,West Ham,3,Watford,1 340 | 21.04.2016,Arsenal,2,West Brom,0 341 | 23.04.2016,Aston Villa,2,Southampton,4 342 | 23.04.2016,Bournemouth,1,Chelsea,4 343 | 23.04.2016,Liverpool,2,Newcastle,2 344 | 23.04.2016,Man City,4,Stoke,0 345 | 24.04.2016,Leicester,4,Swansea,0 346 | 24.04.2016,Sunderland,0,Arsenal,0 347 | 25.04.2016,Tottenham,1,West Brom,1 348 | 30.04.2016,Arsenal,1,Norwich,0 349 | 30.04.2016,Everton,2,Bournemouth,1 350 | 30.04.2016,Newcastle,1,Crystal Palace,0 351 | 30.04.2016,Stoke,1,Sunderland,1 352 | 30.04.2016,Watford,3,Aston Villa,2 353 | 30.04.2016,West Brom,0,West Ham,3 354 | 01.05.2016,Man United,1,Leicester,1 355 | 01.05.2016,Southampton,4,Man City,2 356 | 01.05.2016,Swansea,3,Liverpool,1 357 | 02.05.2016,Chelsea,2,Tottenham,2 358 | 07.05.2016,Aston Villa,0,Newcastle,0 359 | 07.05.2016,Bournemouth,1,West Brom,1 360 | 07.05.2016,Crystal Palace,2,Stoke,1 361 | 07.05.2016,Leicester,3,Everton,1 362 | 07.05.2016,Norwich,0,Man United,1 363 | 07.05.2016,Sunderland,3,Chelsea,2 364 | 07.05.2016,West Ham,1,Swansea,4 365 | 08.05.2016,Liverpool,2,Watford,0 366 | 08.05.2016,Man City,2,Arsenal,2 367 | 08.05.2016,Tottenham,1,Southampton,2 368 | 10.05.2016,West Ham,3,Man United,2 369 | 11.05.2016,Liverpool,1,Chelsea,1 370 | 11.05.2016,Norwich,4,Watford,2 371 | 11.05.2016,Sunderland,3,Everton,0 372 | 15.05.2016,Arsenal,4,Aston Villa,0 373 | 15.05.2016,Chelsea,1,Leicester,1 374 | 15.05.2016,Everton,3,Norwich,0 375 | 15.05.2016,Newcastle,5,Tottenham,1 376 | 15.05.2016,Southampton,4,Crystal Palace,1 377 | 15.05.2016,Stoke,2,West Ham,1 378 | 15.05.2016,Swansea,1,Man City,1 379 | 15.05.2016,Watford,2,Sunderland,2 380 | 15.05.2016,West Brom,1,Liverpool,1 381 | 17.05.2016,Man United,3,Bournemouth,1 -------------------------------------------------------------------------------- /assets/data/formula-one/2016/constructors.csv: -------------------------------------------------------------------------------- 1 | Team,Australia,Bahrain,China,Russia,Spain,Monaco,Canada,Europe,Austria,Great Britain,Hungary,Germany,Belgium,Italy,Singapore,Malaysia,Japan,USA,Mexico,Brazil,Abu Dhabi 2 | Mercedes,43,40,31,43,0,31,35,35,37,40,43,37,40,43,40,15,40,43,43,43,43 3 | Red Bull,12,18,27,0,37,18,18,10,28,30,25,33,18,16,26,43,26,15,27,19,22 4 | Ferrari,15,18,28,15,33,12,26,30,15,12,20,18,10,27,22,12,22,12,18,10,23 5 | Force India,6,0,0,2,6,23,5,17,0,14,1,7,22,5,4,12,10,4,7,18,10 6 | Williams,14,6,9,22,14,1,15,9,2,0,2,2,5,10,0,10,3,6,6,0,2 7 | McLaren,0,1,0,9,2,12,0,0,8,0,6,4,6,0,6,8,0,12,0,1,1 8 | Toro Rosso,3,8,6,0,9,4,2,0,4,5,4,0,0,0,2,0,0,8,0,8,0 9 | Haas,8,10,0,4,0,0,0,0,6,0,0,0,0,0,0,0,0,1,0,0,0 10 | Renault,0,0,0,6,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0 11 | Sauber,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0 12 | Manor,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0 -------------------------------------------------------------------------------- /assets/data/formula-one/2016/drivers.csv: -------------------------------------------------------------------------------- 1 | Driver,Team,Australia,Bahrain,China,Russia,Spain,Monaco,Canada,Europe,Austria,Great Britain,Hungary,Germany,Belgium,Italy,Singapore,Malaysia,Japan,USA,Mexico,Brazil,Abu Dhabi 2 | Lewis Hamilton,Mercedes,18,15,6,18,,25,25,10,25,25,25,25,15,18,15,,15,25,25,25,25 3 | Nico Rosberg,Mercedes,25,25,25,25,,6,10,25,12,15,18,12,25,25,25,15,25,18,18,18,18 4 | Daniel Ricciardo,Red Bull,12,12,12,,12,18,6,6,10,12,15,18,18,10,18,25,8,15,15,4,10 5 | Sebastian Vettel,Ferrari,15,,18,,15,12,18,18,,2,12,10,8,15,10,,12,12,10,10,15 6 | Max Verstappen,Red Bull,1,8,4,,25,,12,4,18,18,10,15,,6,8,18,18,,12,15,12 7 | Kimi Räikkönen,Ferrari,,18,10,15,18,,8,12,15,10,8,8,2,12,12,12,10,,8,,8 8 | Sergio Pérez,Force India,,,,2,6,15,1,15,,8,,1,10,4,4,8,6,4,1,12,4 9 | Valtteri Bottas,Williams,4,2,1,12,10,,15,8,2,,2,2,4,8,,10,1,,4,, 10 | Nico Hülkenberg,Force India,6,,,,,8,4,2,,6,1,6,12,1,,4,4,,6,6,6 11 | Fernando Alonso,McLaren,,,,8,,10,,,,,6,,6,,6,6,,10,,1,1 12 | Felipe Massa,Williams,10,4,8,10,4,1,,1,,,,,1,2,,,2,6,2,,2 13 | Carlos Sainz Jr.,Torro Rosso,2,,2,,8,4,2,,4,4,4,,,,,,,8,,8, 14 | Romain Grosjean,Haas,8,10,,4,,,,,6,,,,,,,,,1,,, 15 | Daniil Kvyat,Torro Rosso,,6,15,,1,,,,,1,,,,,2,,,,,, 16 | Jenson Button,McLaren,,,,1,2,2,,,8,,,4,,,,2,,2,,, 17 | Kevin Magnussen,Renault,,,,6,,,,,,,,,,,1,,,,,, 18 | Felipe Nasr,Sauber,,,,,,,,,,,,,,,,,,,,2, 19 | Jolyon Palmer,Renault,,,,,,,,,,,,,,,,1,,,,, 20 | Pascal Wehrlein,Manor,,,,,,,,,1,,,,,,,,,,,, 21 | Stoffel Vandoorne,McLaren,,1,,,,,,,,,,,,,,,,,,, 22 | Esteban Gutiérrez,Haas,,,,,,,,,,,,,,,,,,,,, 23 | Marcus Ericsson,Sauber,,,,,,,,,,,,,,,,,,,,, 24 | Esteban Ocon,Manor,,,,,,,,,,,,,,,,,,,,, 25 | Rio Haryanto,Manor,,,,,,,,,,,,,,,,,,,,, -------------------------------------------------------------------------------- /assets/data/requests/2016/lebedian-rpfl16-17.csv: -------------------------------------------------------------------------------- 1 | Date,HomeTeam,FTHG,AwayTeam,FTAG 2 | 30.07.16,Зенит,0,Локомотив,0 3 | 30.07.16,Анжи,0,ЦСКА,0 4 | 30.07.16,Ростов,1,Оренбург,0 5 | 31.07.16,Урал,2,Уфа,0 6 | 31.07.16,Спартак,4,Арсенал,0 7 | 31.07.16,Терек,1,Крылья Советов,0 8 | 01.08.16,Рубин,0,Амкар,0 9 | 01.08.16,Краснодар,3,Томь,0 10 | 06.08.16,Уфа,0,Зенит,0 11 | 06.08.16,Арсенал,1,Рубин,0 12 | 07.08.16,Оренбург,0,ЦСКА,1 13 | 07.08.16,Амкар,2,Анжи,0 14 | 07.08.16,Локомотив,2,Томь,2 15 | 07.08.16,Ростов,0,Урал,0 16 | 08.08.16,Спартак,1,Крылья Советов,0 17 | 08.08.16,Краснодар,4,Терек,0 18 | 12.08.16,Зенит,3,Ростов,2 19 | 13.08.16,Урал,0,ЦСКА,1 20 | 13.08.16,Крылья Советов,1,Краснодар,1 21 | 13.08.16,Рубин,1,Спартак,1 22 | 14.08.16,Томь,1,Уфа,0 23 | 14.08.16,Анжи,1,Арсенал,0 24 | 14.08.16,Терек,1,Локомотив,1 25 | 15.08.16,Оренбург,0,Амкар,0 26 | 19.08.16,Рубин,1,Анжи,2 27 | 20.08.16,Уфа,1,Терек,3 28 | 20.08.16,Зенит,1,ЦСКА,1 29 | 20.08.16,Ростов,3,Томь,0 30 | 21.08.16,Амкар,1,Урал,0 31 | 21.08.16,Спартак,2,Краснодар,0 32 | 21.08.16,Локомотив,0,Крылья Советов,0 33 | 22.08.16,Арсенал,0,Оренбург,0 34 | 26.08.16,Крылья Советов,0,Уфа,1 35 | 27.08.16,Оренбург,1,Рубин,1 36 | 27.08.16,Томь,0,ЦСКА,1 37 | 27.08.16,Зенит,3,Амкар,0 38 | 28.08.16,Урал,1,Арсенал,1 39 | 28.08.16,Терек,2,Ростов,1 40 | 28.08.16,Краснодар,1,Локомотив,2 41 | 28.08.16,Анжи,0,Спартак,2 42 | 09.09.16,Ростов,2,Крылья Советов,1 43 | 10.09.16,Амкар,1,Томь,0 44 | 10.09.16,Оренбург,0,Анжи,0 45 | 10.09.16,ЦСКА,3,Терек,0 46 | 11.09.16,Уфа,0,Краснодар,0 47 | 11.09.16,Спартак,1,Локомотив,0 48 | 11.09.16,Арсенал,0,Зенит,5 49 | 12.09.16,Рубин,3,Урал,1 50 | 16.09.16,Оренбург,1,Спартак,3 51 | 17.09.16,Томь,1,Арсенал,0 52 | 17.09.16,Урал,0,Анжи,1 53 | 17.09.16,Терек,1,Амкар,3 54 | 17.09.16,Локомотив,0,Уфа,1 55 | 18.09.16,Крылья Советов,1,ЦСКА,2 56 | 18.09.16,Краснодар,2,Ростов,1 57 | 19.09.16,Зенит,4,Рубин,1 58 | 24.09.16,ЦСКА,1,Краснодар,1 59 | 24.09.16,Ростов,1,Локомотив,0 60 | 25.09.16,Оренбург,0,Урал,1 61 | 25.09.16,Арсенал,0,Терек,0 62 | 25.09.16,Спартак,0,Уфа,1 63 | 25.09.16,Анжи,2,Зенит,2 64 | 26.09.16,Амкар,0,Крылья Советов,0 65 | 26.09.16,Рубин,2,Томь,1 66 | 01.10.16,Томь,1,Урал,1 67 | 01.10.16,Крылья Советов,2,Анжи,1 68 | 01.10.16,Локомотив,1,Арсенал,1 69 | 01.10.16,Терек,2,Оренбург,1 70 | 02.10.16,Уфа,1,Амкар,1 71 | 02.10.16,Зенит,4,Спартак,2 72 | 02.10.16,Краснодар,1,Рубин,0 73 | 02.10.16,Ростов,2,ЦСКА,0 74 | 14.10.16,ЦСКА,1,Уфа,0 75 | 15.10.16,Амкар,0,Локомотив,0 76 | 15.10.16,Рубин,3,Крылья Советов,0 77 | 15.10.16,Спартак,1,Ростов,0 78 | 16.10.16,Урал,0,Зенит,2 79 | 16.10.16,Оренбург,3,Томь,1 80 | 16.10.16,Арсенал,0,Краснодар,0 81 | 17.10.16,Анжи,0,Терек,0 82 | 21.10.16,Крылья Советов,1,Арсенал,1 83 | 22.10.16,Томь,0,Анжи,3 84 | 22.10.16,Уфа,0,Ростов,0 85 | 22.10.16,Урал,0,Спартак,1 86 | 22.10.16,Терек,3,Рубин,1 87 | 23.10.16,Локомотив,1,ЦСКА,0 88 | 23.10.16,Краснодар,1,Амкар,0 89 | 24.10.16,Зенит,1,Оренбург,0 90 | 29.10.16,Амкар,1,Ростов,0 91 | 29.10.16,Спартак,3,ЦСКА,1 92 | 30.10.16,Урал,1,Терек,4 93 | 30.10.16,Арсенал,0,Уфа,2 94 | 30.10.16,Зенит,1,Томь,0 95 | 30.10.16,Анжи,0,Краснодар,0 96 | 31.10.16,Оренбург,1,Крылья Советов,0 97 | 31.10.16,Рубин,2,Локомотив,0 98 | 05.11.16,Томь,0,Спартак,1 99 | 05.11.16,Уфа,2,Рубин,3 100 | 05.11.16,Крылья Советов,2,Урал,2 101 | 05.11.16,Локомотив,4,Анжи,0 102 | 06.11.16,Терек,2,Зенит,1 103 | 06.11.16,Ростов,4,Арсенал,1 104 | 06.11.16,ЦСКА,2,Амкар,2 105 | 06.11.16,Краснодар,3,Оренбург,3 106 | 18.11.16,Арсенал,0,ЦСКА,1 107 | 18.11.16,Рубин,0,Ростов,0 108 | 19.11.16,Оренбург,1,Локомотив,1 109 | 19.11.16,Анжи,0,Уфа,1 110 | 20.11.16,Краснодар,3,Урал,0 111 | 20.11.16,Спартак,1,Амкар,0 112 | 20.11.16,Зенит,3,Крылья Советов,1 113 | 21.11.16,Терек,0,Томь,0 114 | 25.11.16,Уфа,1,Оренбург,0 115 | 26.11.16,Амкар,1,Арсенал,0 116 | 26.11.16,ЦСКА,0,Рубин,0 117 | 26.11.16,Локомотив,1,Урал,1 118 | 26.11.16,Терек,0,Спартак,1 119 | 27.11.16,Крылья Советов,3,Томь,0 120 | 27.11.16,Ростов,2,Анжи,0 121 | 27.11.16,Краснодар,2,Зенит,1 122 | 30.11.16,Урал,1,Ростов,0 123 | 30.11.16,Рубин,1,Арсенал,0 124 | 30.11.16,ЦСКА,2,Оренбург,0 125 | 30.11.16,Зенит,2,Уфа,0 126 | 01.12.16,Крылья Советов,4,Спартак,0 127 | 01.12.16,Терек,2,Краснодар,1 128 | 01.12.16,Томь,1,Локомотив,6 129 | 01.12.16,Анжи,3,Амкар,1 130 | 03.12.16,ЦСКА,4,Урал,0 131 | 03.12.16,Ростов,0,Зенит,0 132 | 04.12.16,Локомотив,2,Терек,0 133 | 05.12.16,Уфа,1,Томь,0 134 | 05.12.16,Амкар,3,Оренбург,0 135 | 05.12.16,Краснодар,1,Крылья Советов,1 136 | 05.12.16,Спартак,2,Рубин,1 137 | 05.12.16,Арсенал,1,Анжи,0 -------------------------------------------------------------------------------- /development.md: -------------------------------------------------------------------------------- 1 | After cloning to a new machine: 2 | `npm install` 3 | 4 | To start the development server: 5 | `npm start` 6 | 7 | To build a new version to use together with website: 8 | `npm run build` 9 | (commit & sync after that) 10 | 11 | To upload new version to CDN: 12 | draft a release on Github. 13 | -------------------------------------------------------------------------------- /dist/assets/data/chgk/2015-2016/student-european-championship.csv: -------------------------------------------------------------------------------- 1 | Команда,Страна,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75 2 | Wings Gaming,Россия,0,1,0,1,1,0,1,1,0,1,0,0,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,1,0,1,1,0,1,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,0,1,1,1,1,0,1,1,0,0,0,0,1,0,1,0 3 | Первая сборная,Россия,1,0,0,1,0,0,1,1,1,1,1,1,1,0,1,1,1,1,0,1,1,1,0,0,1,1,1,1,1,0,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,1,0,1,1,0,0,1,1,1,1,1,0,0,1,1,0,1,0,1,0,0,0,1,1,0,0,1,0 4 | Шесть пик,Россия,0,1,0,1,0,0,1,1,1,1,1,1,0,0,0,1,1,0,1,1,1,1,0,0,1,0,1,1,0,0,1,1,0,1,1,1,1,0,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,0,1,1,1,0,0,0,0,1,0,1,1,1,0,1,0 5 | Мискузи,Россия,1,1,0,1,1,0,1,0,0,1,0,1,0,0,1,1,1,1,0,1,1,1,0,1,1,0,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,0,1,0,0,1,0,1,1,0,1,1,0,1,1,0,1,1,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,0 6 | Цветы,Россия,0,1,0,0,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,1,1,1,0,0,1,0,1,1,1,0,1,1,0,0,1,0,1,0,1,1,0,0,1,0,0,0,1,1,0,0,1,0,1,0,0,1,1,1,1,0,1,1,1,1,0,0,0,1,1,1,0,1,0,1,0 7 | Очень изменилась за лето,Беларусь,0,1,0,1,0,0,0,1,1,1,1,1,0,1,0,1,1,0,0,1,0,1,0,1,1,1,0,1,0,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,0,1,1,1,1,1,0,0,1,0,0,0,1,0,0,0,1,1,1,1,0,0,0,0,1,1,0,1,0,1,0 8 | Рыболюди,Украина,0,1,0,1,0,0,0,0,1,1,1,1,0,1,0,1,1,0,0,1,1,1,0,1,1,0,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,0,1,0,1,1,0,0,1,1,1,1,0,0,1,1,0,0,1,1,1,1,1,0,1,0,0,0,0,0,0,0,1,0 9 | White and nerdy,Россия,1,1,0,1,0,0,1,1,1,1,1,0,0,1,0,1,1,1,0,1,1,1,0,0,1,0,1,1,1,0,1,1,0,0,1,1,0,1,1,1,0,0,0,0,1,0,1,1,1,0,1,1,1,1,1,1,0,0,1,0,1,0,0,1,0,0,0,1,1,0,0,0,0,1,0 10 | Трактор в поле сыр-сыр-сыр,Россия,0,1,0,0,1,1,0,0,1,1,1,1,0,1,1,1,1,1,0,1,0,1,0,1,1,1,0,1,0,0,1,1,0,0,1,1,1,1,0,0,1,0,0,0,0,0,1,1,1,1,1,0,1,1,0,1,1,1,1,0,1,0,0,0,0,0,1,0,0,1,0,1,0,1,0 11 | Корпрусариум,Беларусь,1,1,0,0,1,0,0,0,1,0,0,1,0,0,0,1,1,0,1,1,1,1,0,1,1,0,1,0,1,0,1,1,0,0,1,0,1,1,0,1,1,0,1,1,0,1,1,1,0,0,1,0,1,0,1,0,1,1,0,0,1,1,1,0,0,0,0,1,0,1,1,0,0,1,0 12 | На заре,Беларусь,1,1,0,0,0,0,0,1,0,0,1,1,1,0,0,1,1,0,0,0,0,1,0,1,1,1,0,1,1,0,0,1,0,1,1,0,1,1,0,1,1,0,1,0,0,1,0,0,0,1,1,0,1,0,1,1,1,1,1,0,1,1,1,0,0,0,0,1,1,1,0,0,0,1,0 13 | Имитируем сарказм,Беларусь,0,1,0,1,0,0,1,0,1,1,1,1,1,0,1,1,0,1,0,1,1,1,0,0,0,0,0,1,1,0,1,1,1,0,1,0,0,0,0,0,1,0,1,0,0,0,1,0,1,1,1,0,0,0,1,0,1,0,0,1,1,1,0,1,0,0,0,1,0,1,0,1,0,1,0 14 | Комплексные URумбаи,Беларусь,0,1,0,0,1,0,0,1,0,1,0,1,1,0,0,1,0,0,0,1,1,1,0,1,1,0,0,1,0,0,0,1,1,0,1,1,1,0,0,0,1,0,1,1,1,1,1,0,0,1,1,0,1,0,0,0,1,1,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,1,0 15 | Ультиматум Дорна,Украина,1,1,1,0,1,1,0,0,0,0,0,1,0,0,0,1,0,0,1,0,0,1,0,0,1,1,1,1,1,0,1,0,0,1,1,1,0,1,1,0,1,0,0,0,0,1,0,0,1,1,0,0,1,0,1,1,1,0,0,0,1,1,1,0,0,1,0,1,0,0,0,0,0,1,0 16 | Altavista,Беларусь,0,1,0,1,1,1,0,0,1,1,1,1,1,0,0,1,1,1,0,1,0,1,0,1,0,0,1,1,0,0,0,1,1,0,1,0,0,0,1,0,1,0,0,0,0,1,0,0,0,1,1,0,1,1,1,0,0,1,0,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0 17 | Sortasidra,Россия,1,1,0,0,1,0,1,0,1,0,1,1,0,0,0,1,0,0,0,0,1,1,0,1,0,0,0,1,1,1,0,1,0,0,1,1,1,1,0,0,0,0,0,0,1,1,0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,0,0,0,0,0,0,1,0,0,0,1,0 18 | Колянизация,Украина,0,1,0,1,0,0,1,0,1,0,1,1,1,0,0,1,1,0,0,1,0,1,0,1,0,1,0,1,0,0,1,1,0,0,1,1,0,1,0,0,1,0,0,0,0,1,0,0,1,1,0,0,0,1,1,0,0,0,0,0,1,0,1,0,1,1,0,0,0,0,0,1,0,1,0 19 | Donkey Hot,Украина,0,1,0,1,1,1,1,0,1,0,0,1,0,0,0,1,1,1,0,0,0,1,0,1,0,0,1,1,1,0,1,0,1,0,1,1,1,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,1,0 20 | Сборная Армении,Армения,0,0,0,1,0,0,0,0,1,0,0,1,1,1,0,1,1,0,1,1,0,1,0,1,0,1,0,1,0,0,0,1,1,0,0,1,0,0,0,1,1,0,1,0,0,0,1,0,1,0,0,0,0,1,0,0,0,1,1,1,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0 21 | Британские учёные,Латвия,1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,1,1,1,1,1,1,0,0,0,0,1,1,0,0,1,0,1,0,1,0,1,1,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,1,0 22 | Восточный Мордор,Украина,0,1,0,0,0,0,0,0,1,0,1,1,1,0,0,1,0,1,0,1,1,1,0,1,1,0,1,1,0,0,0,0,1,0,1,1,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,1,0,0,1,0,0,1,1,0,0,0,0,1,0,0,1,0,1,0,0,0 23 | Холодец безжалостный,Россия,0,1,0,1,0,0,1,0,1,0,0,0,0,0,0,1,1,0,0,1,0,1,0,0,1,0,1,1,1,0,0,1,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,0,1,0,0,0 24 | Эстонский экспресс,Беларусь,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,1,0,0,0,0,0,0,0,0,1,1,1,0,1,1,0,0,0,0,0,0,0,1,0,1,1,0,1,0,1,1,0,0,0,0,1,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,1,0 25 | Сборная Молдовы,Молдова,0,1,0,1,0,0,0,0,0,0,0,0,1,0,0,1,1,0,1,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,1,0,0,1,1,0,0,0,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0 -------------------------------------------------------------------------------- /dist/assets/data/football/2015-2016/english-premier-league.csv: -------------------------------------------------------------------------------- 1 | Date,HomeTeam,FTHG,AwayTeam,FTAG 2 | 08.08.2015,Bournemouth,0,Aston Villa,1 3 | 08.08.2015,Chelsea,2,Swansea,2 4 | 08.08.2015,Everton,2,Watford,2 5 | 08.08.2015,Leicester,4,Sunderland,2 6 | 08.08.2015,Man United,1,Tottenham,0 7 | 08.08.2015,Norwich,1,Crystal Palace,3 8 | 09.08.2015,Arsenal,0,West Ham,2 9 | 09.08.2015,Newcastle,2,Southampton,2 10 | 09.08.2015,Stoke,0,Liverpool,1 11 | 10.08.2015,West Brom,0,Man City,3 12 | 14.08.2015,Aston Villa,0,Man United,1 13 | 15.08.2015,Southampton,0,Everton,3 14 | 15.08.2015,Sunderland,1,Norwich,3 15 | 15.08.2015,Swansea,2,Newcastle,0 16 | 15.08.2015,Tottenham,2,Stoke,2 17 | 15.08.2015,Watford,0,West Brom,0 18 | 15.08.2015,West Ham,1,Leicester,2 19 | 16.08.2015,Crystal Palace,1,Arsenal,2 20 | 16.08.2015,Man City,3,Chelsea,0 21 | 17.08.2015,Liverpool,1,Bournemouth,0 22 | 22.08.2015,Crystal Palace,2,Aston Villa,1 23 | 22.08.2015,Leicester,1,Tottenham,1 24 | 22.08.2015,Man United,0,Newcastle,0 25 | 22.08.2015,Norwich,1,Stoke,1 26 | 22.08.2015,Sunderland,1,Swansea,1 27 | 22.08.2015,West Ham,3,Bournemouth,4 28 | 23.08.2015,Everton,0,Man City,2 29 | 23.08.2015,Watford,0,Southampton,0 30 | 23.08.2015,West Brom,2,Chelsea,3 31 | 24.08.2015,Arsenal,0,Liverpool,0 32 | 29.08.2015,Aston Villa,2,Sunderland,2 33 | 29.08.2015,Bournemouth,1,Leicester,1 34 | 29.08.2015,Chelsea,1,Crystal Palace,2 35 | 29.08.2015,Liverpool,0,West Ham,3 36 | 29.08.2015,Man City,2,Watford,0 37 | 29.08.2015,Newcastle,0,Arsenal,1 38 | 29.08.2015,Stoke,0,West Brom,1 39 | 29.08.2015,Tottenham,0,Everton,0 40 | 30.08.2015,Southampton,3,Norwich,0 41 | 30.08.2015,Swansea,2,Man United,1 42 | 12.09.2015,Arsenal,2,Stoke,0 43 | 12.09.2015,Crystal Palace,0,Man City,1 44 | 12.09.2015,Everton,3,Chelsea,1 45 | 12.09.2015,Man United,3,Liverpool,1 46 | 12.09.2015,Norwich,3,Bournemouth,1 47 | 12.09.2015,Watford,1,Swansea,0 48 | 12.09.2015,West Brom,0,Southampton,0 49 | 13.09.2015,Leicester,3,Aston Villa,2 50 | 13.09.2015,Sunderland,0,Tottenham,1 51 | 14.09.2015,West Ham,2,Newcastle,0 52 | 19.09.2015,Aston Villa,0,West Brom,1 53 | 19.09.2015,Bournemouth,2,Sunderland,0 54 | 19.09.2015,Chelsea,2,Arsenal,0 55 | 19.09.2015,Man City,1,West Ham,2 56 | 19.09.2015,Newcastle,1,Watford,2 57 | 19.09.2015,Stoke,2,Leicester,2 58 | 19.09.2015,Swansea,0,Everton,0 59 | 20.09.2015,Liverpool,1,Norwich,1 60 | 20.09.2015,Southampton,2,Man United,3 61 | 20.09.2015,Tottenham,1,Crystal Palace,0 62 | 26.09.2015,Leicester,2,Arsenal,5 63 | 26.09.2015,Liverpool,3,Aston Villa,2 64 | 26.09.2015,Man United,3,Sunderland,0 65 | 26.09.2015,Newcastle,2,Chelsea,2 66 | 26.09.2015,Southampton,3,Swansea,1 67 | 26.09.2015,Stoke,2,Bournemouth,1 68 | 26.09.2015,Tottenham,4,Man City,1 69 | 26.09.2015,West Ham,2,Norwich,2 70 | 27.09.2015,Watford,0,Crystal Palace,1 71 | 28.09.2015,West Brom,2,Everton,3 72 | 03.10.2015,Aston Villa,0,Stoke,1 73 | 03.10.2015,Bournemouth,1,Watford,1 74 | 03.10.2015,Chelsea,1,Southampton,3 75 | 03.10.2015,Crystal Palace,2,West Brom,0 76 | 03.10.2015,Man City,6,Newcastle,1 77 | 03.10.2015,Norwich,1,Leicester,2 78 | 03.10.2015,Sunderland,2,West Ham,2 79 | 04.10.2015,Arsenal,3,Man United,0 80 | 04.10.2015,Everton,1,Liverpool,1 81 | 04.10.2015,Swansea,2,Tottenham,2 82 | 17.10.2015,Chelsea,2,Aston Villa,0 83 | 17.10.2015,Crystal Palace,1,West Ham,3 84 | 17.10.2015,Everton,0,Man United,3 85 | 17.10.2015,Man City,5,Bournemouth,1 86 | 17.10.2015,Southampton,2,Leicester,2 87 | 17.10.2015,Tottenham,0,Liverpool,0 88 | 17.10.2015,Watford,0,Arsenal,3 89 | 17.10.2015,West Brom,1,Sunderland,0 90 | 18.10.2015,Newcastle,6,Norwich,2 91 | 19.10.2015,Swansea,0,Stoke,1 92 | 24.10.2015,Arsenal,2,Everton,1 93 | 24.10.2015,Aston Villa,1,Swansea,2 94 | 24.10.2015,Leicester,1,Crystal Palace,0 95 | 24.10.2015,Norwich,0,West Brom,1 96 | 24.10.2015,Stoke,0,Watford,2 97 | 24.10.2015,West Ham,2,Chelsea,1 98 | 25.10.2015,Bournemouth,1,Tottenham,5 99 | 25.10.2015,Liverpool,1,Southampton,1 100 | 25.10.2015,Man United,0,Man City,0 101 | 25.10.2015,Sunderland,3,Newcastle,0 102 | 31.10.2015,Chelsea,1,Liverpool,3 103 | 31.10.2015,Crystal Palace,0,Man United,0 104 | 31.10.2015,Man City,2,Norwich,1 105 | 31.10.2015,Newcastle,0,Stoke,0 106 | 31.10.2015,Swansea,0,Arsenal,3 107 | 31.10.2015,Watford,2,West Ham,0 108 | 31.10.2015,West Brom,2,Leicester,3 109 | 01.11.2015,Everton,6,Sunderland,2 110 | 01.11.2015,Southampton,2,Bournemouth,0 111 | 02.11.2015,Tottenham,3,Aston Villa,1 112 | 07.11.2015,Bournemouth,0,Newcastle,1 113 | 07.11.2015,Leicester,2,Watford,1 114 | 07.11.2015,Man United,2,West Brom,0 115 | 07.11.2015,Norwich,1,Swansea,0 116 | 07.11.2015,Stoke,1,Chelsea,0 117 | 07.11.2015,Sunderland,0,Southampton,1 118 | 07.11.2015,West Ham,1,Everton,1 119 | 08.11.2015,Arsenal,1,Tottenham,1 120 | 08.11.2015,Aston Villa,0,Man City,0 121 | 08.11.2015,Liverpool,1,Crystal Palace,2 122 | 21.11.2015,Chelsea,1,Norwich,0 123 | 21.11.2015,Everton,4,Aston Villa,0 124 | 21.11.2015,Man City,1,Liverpool,4 125 | 21.11.2015,Newcastle,0,Leicester,3 126 | 21.11.2015,Southampton,0,Stoke,1 127 | 21.11.2015,Swansea,2,Bournemouth,2 128 | 21.11.2015,Watford,1,Man United,2 129 | 21.11.2015,West Brom,2,Arsenal,1 130 | 22.11.2015,Tottenham,4,West Ham,1 131 | 23.11.2015,Crystal Palace,0,Sunderland,1 132 | 28.11.2015,Aston Villa,2,Watford,3 133 | 28.11.2015,Bournemouth,3,Everton,3 134 | 28.11.2015,Crystal Palace,5,Newcastle,1 135 | 28.11.2015,Leicester,1,Man United,1 136 | 28.11.2015,Man City,3,Southampton,1 137 | 28.11.2015,Sunderland,2,Stoke,0 138 | 29.11.2015,Liverpool,1,Swansea,0 139 | 29.11.2015,Norwich,1,Arsenal,1 140 | 29.11.2015,Tottenham,0,Chelsea,0 141 | 29.11.2015,West Ham,1,West Brom,1 142 | 05.12.2015,Arsenal,3,Sunderland,1 143 | 05.12.2015,Chelsea,0,Bournemouth,1 144 | 05.12.2015,Man United,0,West Ham,0 145 | 05.12.2015,Southampton,1,Aston Villa,1 146 | 05.12.2015,Stoke,2,Man City,0 147 | 05.12.2015,Swansea,0,Leicester,3 148 | 05.12.2015,Watford,2,Norwich,0 149 | 05.12.2015,West Brom,1,Tottenham,1 150 | 06.12.2015,Newcastle,2,Liverpool,0 151 | 07.12.2015,Everton,1,Crystal Palace,1 152 | 12.12.2015,Bournemouth,2,Man United,1 153 | 12.12.2015,Crystal Palace,1,Southampton,0 154 | 12.12.2015,Man City,2,Swansea,1 155 | 12.12.2015,Norwich,1,Everton,1 156 | 12.12.2015,Sunderland,0,Watford,1 157 | 12.12.2015,West Ham,0,Stoke,0 158 | 13.12.2015,Aston Villa,0,Arsenal,2 159 | 13.12.2015,Liverpool,2,West Brom,2 160 | 13.12.2015,Tottenham,1,Newcastle,2 161 | 14.12.2015,Leicester,2,Chelsea,1 162 | 19.12.2015,Chelsea,3,Sunderland,1 163 | 19.12.2015,Everton,2,Leicester,3 164 | 19.12.2015,Man United,1,Norwich,2 165 | 19.12.2015,Newcastle,1,Aston Villa,1 166 | 19.12.2015,Southampton,0,Tottenham,2 167 | 19.12.2015,Stoke,1,Crystal Palace,2 168 | 19.12.2015,West Brom,1,Bournemouth,2 169 | 20.12.2015,Swansea,0,West Ham,0 170 | 20.12.2015,Watford,3,Liverpool,0 171 | 21.12.2015,Arsenal,2,Man City,1 172 | 26.12.2015,Aston Villa,1,West Ham,1 173 | 26.12.2015,Bournemouth,0,Crystal Palace,0 174 | 26.12.2015,Chelsea,2,Watford,2 175 | 26.12.2015,Liverpool,1,Leicester,0 176 | 26.12.2015,Man City,4,Sunderland,1 177 | 26.12.2015,Newcastle,0,Everton,1 178 | 26.12.2015,Southampton,4,Arsenal,0 179 | 26.12.2015,Stoke,2,Man United,0 180 | 26.12.2015,Swansea,1,West Brom,0 181 | 26.12.2015,Tottenham,3,Norwich,0 182 | 28.12.2015,Arsenal,2,Bournemouth,0 183 | 28.12.2015,Crystal Palace,0,Swansea,0 184 | 28.12.2015,Everton,3,Stoke,4 185 | 28.12.2015,Man United,0,Chelsea,0 186 | 28.12.2015,Norwich,2,Aston Villa,0 187 | 28.12.2015,Watford,1,Tottenham,2 188 | 28.12.2015,West Brom,1,Newcastle,0 189 | 28.12.2015,West Ham,2,Southampton,1 190 | 29.12.2015,Leicester,0,Man City,0 191 | 30.12.2015,Sunderland,0,Liverpool,1 192 | 02.01.2016,Arsenal,1,Newcastle,0 193 | 02.01.2016,Leicester,0,Bournemouth,0 194 | 02.01.2016,Man United,2,Swansea,1 195 | 02.01.2016,Norwich,1,Southampton,0 196 | 02.01.2016,Sunderland,3,Aston Villa,1 197 | 02.01.2016,Watford,1,Man City,2 198 | 02.01.2016,West Brom,2,Stoke,1 199 | 02.01.2016,West Ham,2,Liverpool,0 200 | 03.01.2016,Crystal Palace,0,Chelsea,3 201 | 03.01.2016,Everton,1,Tottenham,1 202 | 12.01.2016,Aston Villa,1,Crystal Palace,0 203 | 12.01.2016,Bournemouth,1,West Ham,3 204 | 12.01.2016,Newcastle,3,Man United,3 205 | 13.01.2016,Chelsea,2,West Brom,2 206 | 13.01.2016,Liverpool,3,Arsenal,3 207 | 13.01.2016,Man City,0,Everton,0 208 | 13.01.2016,Southampton,2,Watford,0 209 | 13.01.2016,Stoke,3,Norwich,1 210 | 13.01.2016,Swansea,2,Sunderland,4 211 | 13.01.2016,Tottenham,0,Leicester,1 212 | 16.01.2016,Aston Villa,1,Leicester,1 213 | 16.01.2016,Bournemouth,3,Norwich,0 214 | 16.01.2016,Chelsea,3,Everton,3 215 | 16.01.2016,Man City,4,Crystal Palace,0 216 | 16.01.2016,Newcastle,2,West Ham,1 217 | 16.01.2016,Southampton,3,West Brom,0 218 | 16.01.2016,Tottenham,4,Sunderland,1 219 | 17.01.2016,Liverpool,0,Man United,1 220 | 17.01.2016,Stoke,0,Arsenal,0 221 | 18.01.2016,Swansea,1,Watford,0 222 | 23.01.2016,Crystal Palace,1,Tottenham,3 223 | 23.01.2016,Leicester,3,Stoke,0 224 | 23.01.2016,Man United,0,Southampton,1 225 | 23.01.2016,Norwich,4,Liverpool,5 226 | 23.01.2016,Sunderland,1,Bournemouth,1 227 | 23.01.2016,Watford,2,Newcastle,1 228 | 23.01.2016,West Brom,0,Aston Villa,0 229 | 23.01.2016,West Ham,2,Man City,2 230 | 24.01.2016,Arsenal,0,Chelsea,1 231 | 24.01.2016,Everton,1,Swansea,2 232 | 02.02.2016,Arsenal,0,Southampton,0 233 | 02.02.2016,Crystal Palace,1,Bournemouth,2 234 | 02.02.2016,Leicester,2,Liverpool,0 235 | 02.02.2016,Man United,3,Stoke,0 236 | 02.02.2016,Norwich,0,Tottenham,3 237 | 02.02.2016,Sunderland,0,Man City,1 238 | 02.02.2016,West Brom,1,Swansea,1 239 | 02.02.2016,West Ham,2,Aston Villa,0 240 | 03.02.2016,Everton,3,Newcastle,0 241 | 03.02.2016,Watford,0,Chelsea,0 242 | 06.02.2016,Aston Villa,2,Norwich,0 243 | 06.02.2016,Liverpool,2,Sunderland,2 244 | 06.02.2016,Man City,1,Leicester,3 245 | 06.02.2016,Newcastle,1,West Brom,0 246 | 06.02.2016,Southampton,1,West Ham,0 247 | 06.02.2016,Stoke,0,Everton,3 248 | 06.02.2016,Swansea,1,Crystal Palace,1 249 | 06.02.2016,Tottenham,1,Watford,0 250 | 07.02.2016,Bournemouth,0,Arsenal,2 251 | 07.02.2016,Chelsea,1,Man United,1 252 | 13.02.2016,Bournemouth,1,Stoke,3 253 | 13.02.2016,Chelsea,5,Newcastle,1 254 | 13.02.2016,Crystal Palace,1,Watford,2 255 | 13.02.2016,Everton,0,West Brom,1 256 | 13.02.2016,Norwich,2,West Ham,2 257 | 13.02.2016,Sunderland,2,Man United,1 258 | 13.02.2016,Swansea,0,Southampton,1 259 | 14.02.2016,Arsenal,2,Leicester,1 260 | 14.02.2016,Aston Villa,0,Liverpool,6 261 | 14.02.2016,Man City,1,Tottenham,2 262 | 27.02.2016,Leicester,1,Norwich,0 263 | 27.02.2016,Southampton,1,Chelsea,2 264 | 27.02.2016,Stoke,2,Aston Villa,1 265 | 27.02.2016,Watford,0,Bournemouth,0 266 | 27.02.2016,West Brom,3,Crystal Palace,2 267 | 27.02.2016,West Ham,1,Sunderland,0 268 | 28.02.2016,Tottenham,2,Swansea,1 269 | 28.02.2016,Man United,3,Arsenal,2 270 | 01.03.2016,Aston Villa,1,Everton,3 271 | 01.03.2016,Bournemouth,2,Southampton,0 272 | 01.03.2016,Leicester,2,West Brom,2 273 | 01.03.2016,Norwich,1,Chelsea,2 274 | 01.03.2016,Sunderland,2,Crystal Palace,2 275 | 02.03.2016,Arsenal,1,Swansea,2 276 | 02.03.2016,Liverpool,3,Man City,0 277 | 02.03.2016,Man United,1,Watford,0 278 | 02.03.2016,Stoke,1,Newcastle,0 279 | 02.03.2016,West Ham,1,Tottenham,0 280 | 05.03.2016,Chelsea,1,Stoke,1 281 | 05.03.2016,Everton,2,West Ham,3 282 | 05.03.2016,Man City,4,Aston Villa,0 283 | 05.03.2016,Newcastle,1,Bournemouth,3 284 | 05.03.2016,Southampton,1,Sunderland,1 285 | 05.03.2016,Swansea,1,Norwich,0 286 | 05.03.2016,Tottenham,2,Arsenal,2 287 | 05.03.2016,Watford,0,Leicester,1 288 | 06.03.2016,Crystal Palace,1,Liverpool,2 289 | 06.03.2016,West Brom,1,Man United,0 290 | 12.03.2016,Bournemouth,3,Swansea,2 291 | 12.03.2016,Norwich,0,Man City,0 292 | 12.03.2016,Stoke,1,Southampton,2 293 | 13.03.2016,Aston Villa,0,Tottenham,2 294 | 14.03.2016,Leicester,1,Newcastle,0 295 | 19.03.2016,Chelsea,2,West Ham,2 296 | 19.03.2016,Crystal Palace,0,Leicester,1 297 | 19.03.2016,Everton,0,Arsenal,2 298 | 19.03.2016,Swansea,1,Aston Villa,0 299 | 19.03.2016,Watford,1,Stoke,2 300 | 19.03.2016,West Brom,0,Norwich,1 301 | 20.03.2016,Man City,0,Man United,1 302 | 20.03.2016,Newcastle,1,Sunderland,1 303 | 20.03.2016,Southampton,3,Liverpool,2 304 | 20.03.2016,Tottenham,3,Bournemouth,0 305 | 02.04.2016,Arsenal,4,Watford,0 306 | 02.04.2016,Aston Villa,0,Chelsea,4 307 | 02.04.2016,Bournemouth,0,Man City,4 308 | 02.04.2016,Liverpool,1,Tottenham,1 309 | 02.04.2016,Norwich,3,Newcastle,2 310 | 02.04.2016,Stoke,2,Swansea,2 311 | 02.04.2016,Sunderland,0,West Brom,0 312 | 02.04.2016,West Ham,2,Crystal Palace,2 313 | 03.04.2016,Leicester,1,Southampton,0 314 | 03.04.2016,Man United,1,Everton,0 315 | 09.04.2016,Aston Villa,1,Bournemouth,2 316 | 09.04.2016,Crystal Palace,1,Norwich,0 317 | 09.04.2016,Man City,2,West Brom,1 318 | 09.04.2016,Southampton,3,Newcastle,1 319 | 09.04.2016,Swansea,1,Chelsea,0 320 | 09.04.2016,Watford,1,Everton,1 321 | 09.04.2016,West Ham,3,Arsenal,3 322 | 10.04.2016,Liverpool,4,Stoke,1 323 | 10.04.2016,Sunderland,0,Leicester,2 324 | 10.04.2016,Tottenham,3,Man United,0 325 | 13.04.2016,Crystal Palace,0,Everton,0 326 | 16.04.2016,Chelsea,0,Man City,3 327 | 16.04.2016,Everton,1,Southampton,1 328 | 16.04.2016,Man United,1,Aston Villa,0 329 | 16.04.2016,Newcastle,3,Swansea,0 330 | 16.04.2016,Norwich,0,Sunderland,3 331 | 16.04.2016,West Brom,0,Watford,1 332 | 17.04.2016,Arsenal,1,Crystal Palace,1 333 | 17.04.2016,Bournemouth,1,Liverpool,2 334 | 17.04.2016,Leicester,2,West Ham,2 335 | 18.04.2016,Stoke,0,Tottenham,4 336 | 19.04.2016,Newcastle,1,Man City,1 337 | 20.04.2016,Liverpool,4,Everton,0 338 | 20.04.2016,Man United,2,Crystal Palace,0 339 | 20.04.2016,West Ham,3,Watford,1 340 | 21.04.2016,Arsenal,2,West Brom,0 341 | 23.04.2016,Aston Villa,2,Southampton,4 342 | 23.04.2016,Bournemouth,1,Chelsea,4 343 | 23.04.2016,Liverpool,2,Newcastle,2 344 | 23.04.2016,Man City,4,Stoke,0 345 | 24.04.2016,Leicester,4,Swansea,0 346 | 24.04.2016,Sunderland,0,Arsenal,0 347 | 25.04.2016,Tottenham,1,West Brom,1 348 | 30.04.2016,Arsenal,1,Norwich,0 349 | 30.04.2016,Everton,2,Bournemouth,1 350 | 30.04.2016,Newcastle,1,Crystal Palace,0 351 | 30.04.2016,Stoke,1,Sunderland,1 352 | 30.04.2016,Watford,3,Aston Villa,2 353 | 30.04.2016,West Brom,0,West Ham,3 354 | 01.05.2016,Man United,1,Leicester,1 355 | 01.05.2016,Southampton,4,Man City,2 356 | 01.05.2016,Swansea,3,Liverpool,1 357 | 02.05.2016,Chelsea,2,Tottenham,2 358 | 07.05.2016,Aston Villa,0,Newcastle,0 359 | 07.05.2016,Bournemouth,1,West Brom,1 360 | 07.05.2016,Crystal Palace,2,Stoke,1 361 | 07.05.2016,Leicester,3,Everton,1 362 | 07.05.2016,Norwich,0,Man United,1 363 | 07.05.2016,Sunderland,3,Chelsea,2 364 | 07.05.2016,West Ham,1,Swansea,4 365 | 08.05.2016,Liverpool,2,Watford,0 366 | 08.05.2016,Man City,2,Arsenal,2 367 | 08.05.2016,Tottenham,1,Southampton,2 368 | 10.05.2016,West Ham,3,Man United,2 369 | 11.05.2016,Liverpool,1,Chelsea,1 370 | 11.05.2016,Norwich,4,Watford,2 371 | 11.05.2016,Sunderland,3,Everton,0 372 | 15.05.2016,Arsenal,4,Aston Villa,0 373 | 15.05.2016,Chelsea,1,Leicester,1 374 | 15.05.2016,Everton,3,Norwich,0 375 | 15.05.2016,Newcastle,5,Tottenham,1 376 | 15.05.2016,Southampton,4,Crystal Palace,1 377 | 15.05.2016,Stoke,2,West Ham,1 378 | 15.05.2016,Swansea,1,Man City,1 379 | 15.05.2016,Watford,2,Sunderland,2 380 | 15.05.2016,West Brom,1,Liverpool,1 381 | 17.05.2016,Man United,3,Bournemouth,1 -------------------------------------------------------------------------------- /dist/assets/data/formula-one/2016/drivers.csv: -------------------------------------------------------------------------------- 1 | Driver,Team,Australia,Bahrain,China,Russia,Spain,Monaco,Canada,Europe,Austria,Great Britain,Hungary,Germany,Belgium,Italy,Singapore,Malaysia,Japan,USA,Mexico,Brazil,Abu Dhabi 2 | Lewis Hamilton,Mercedes,18,15,6,18,,25,25,10,25,25,25,25,15,18,15,,15,25,25,25,25 3 | Nico Rosberg,Mercedes,25,25,25,25,,6,10,25,12,15,18,12,25,25,25,15,25,18,18,18,18 4 | Daniel Ricciardo,Red Bull,12,12,12,,12,18,6,6,10,12,15,18,18,10,18,25,8,15,15,4,10 5 | Sebastian Vettel,Ferrari,15,,18,,15,12,18,18,,2,12,10,8,15,10,,12,12,10,10,15 6 | Max Verstappen,Red Bull,1,8,4,,25,,12,4,18,18,10,15,,6,8,18,18,,12,15,12 7 | Kimi Räikkönen,Ferrari,,18,10,15,18,,8,12,15,10,8,8,2,12,12,12,10,,8,,8 8 | Sergio Pérez,Force India,,,,2,6,15,1,15,,8,,1,10,4,4,8,6,4,1,12,4 9 | Valtteri Bottas,Williams,4,2,1,12,10,,15,8,2,,2,2,4,8,,10,1,,4,, 10 | Nico Hülkenberg,Force India,6,,,,,8,4,2,,6,1,6,12,1,,4,4,,6,6,6 11 | Fernando Alonso,McLaren,,,,8,,10,,,,,6,,6,,6,6,,10,,1,1 12 | Felipe Massa,Williams,10,4,8,10,4,1,,1,,,,,1,2,,,2,6,2,,2 13 | Carlos Sainz Jr.,Torro Rosso,2,,2,,8,4,2,,4,4,4,,,,,,,8,,8, 14 | Romain Grosjean,Haas,8,10,,4,,,,,6,,,,,,,,,1,,, 15 | Daniil Kvyat,Torro Rosso,,6,15,,1,,,,,1,,,,,2,,,,,, 16 | Jenson Button,McLaren,,,,1,2,2,,,8,,,4,,,,2,,2,,, 17 | Kevin Magnussen,Renault,,,,6,,,,,,,,,,,1,,,,,, 18 | Felipe Nasr,Sauber,,,,,,,,,,,,,,,,,,,,2, 19 | Jolyon Palmer,Renault,,,,,,,,,,,,,,,,1,,,,, 20 | Pascal Wehrlein,Manor,,,,,,,,,1,,,,,,,,,,,, 21 | Stoffel Vandoorne,McLaren,,1,,,,,,,,,,,,,,,,,,, 22 | Esteban Gutiérrez,Haas,,,,,,,,,,,,,,,,,,,,, 23 | Marcus Ericsson,Sauber,,,,,,,,,,,,,,,,,,,,, 24 | Esteban Ocon,Manor,,,,,,,,,,,,,,,,,,,,, 25 | Rio Haryanto,Manor,,,,,,,,,,,,,,,,,,,,, -------------------------------------------------------------------------------- /dist/assets/data/requests/2016/lebedian-rpfl16-17.csv: -------------------------------------------------------------------------------- 1 | Date,HomeTeam,FTHG,AwayTeam,FTAG 2 | 30.07.16,Зенит,0,Локомотив,0 3 | 30.07.16,Анжи,0,ЦСКА,0 4 | 30.07.16,Ростов,1,Оренбург,0 5 | 31.07.16,Урал,2,Уфа,0 6 | 31.07.16,Спартак,4,Арсенал,0 7 | 31.07.16,Терек,1,Крылья Советов,0 8 | 01.08.16,Рубин,0,Амкар,0 9 | 01.08.16,Краснодар,3,Томь,0 10 | 06.08.16,Уфа,0,Зенит,0 11 | 06.08.16,Арсенал,1,Рубин,0 12 | 07.08.16,Оренбург,0,ЦСКА,1 13 | 07.08.16,Амкар,2,Анжи,0 14 | 07.08.16,Локомотив,2,Томь,2 15 | 07.08.16,Ростов,0,Урал,0 16 | 08.08.16,Спартак,1,Крылья Советов,0 17 | 08.08.16,Краснодар,4,Терек,0 18 | 12.08.16,Зенит,3,Ростов,2 19 | 13.08.16,Урал,0,ЦСКА,1 20 | 13.08.16,Крылья Советов,1,Краснодар,1 21 | 13.08.16,Рубин,1,Спартак,1 22 | 14.08.16,Томь,1,Уфа,0 23 | 14.08.16,Анжи,1,Арсенал,0 24 | 14.08.16,Терек,1,Локомотив,1 25 | 15.08.16,Оренбург,0,Амкар,0 26 | 19.08.16,Рубин,1,Анжи,2 27 | 20.08.16,Уфа,1,Терек,3 28 | 20.08.16,Зенит,1,ЦСКА,1 29 | 20.08.16,Ростов,3,Томь,0 30 | 21.08.16,Амкар,1,Урал,0 31 | 21.08.16,Спартак,2,Краснодар,0 32 | 21.08.16,Локомотив,0,Крылья Советов,0 33 | 22.08.16,Арсенал,0,Оренбург,0 34 | 26.08.16,Крылья Советов,0,Уфа,1 35 | 27.08.16,Оренбург,1,Рубин,1 36 | 27.08.16,Томь,0,ЦСКА,1 37 | 27.08.16,Зенит,3,Амкар,0 38 | 28.08.16,Урал,1,Арсенал,1 39 | 28.08.16,Терек,2,Ростов,1 40 | 28.08.16,Краснодар,1,Локомотив,2 41 | 28.08.16,Анжи,0,Спартак,2 42 | 09.09.16,Ростов,2,Крылья Советов,1 43 | 10.09.16,Амкар,1,Томь,0 44 | 10.09.16,Оренбург,0,Анжи,0 45 | 10.09.16,ЦСКА,3,Терек,0 46 | 11.09.16,Уфа,0,Краснодар,0 47 | 11.09.16,Спартак,1,Локомотив,0 48 | 11.09.16,Арсенал,0,Зенит,5 49 | 12.09.16,Рубин,3,Урал,1 50 | 16.09.16,Оренбург,1,Спартак,3 51 | 17.09.16,Томь,1,Арсенал,0 52 | 17.09.16,Урал,0,Анжи,1 53 | 17.09.16,Терек,1,Амкар,3 54 | 17.09.16,Локомотив,0,Уфа,1 55 | 18.09.16,Крылья Советов,1,ЦСКА,2 56 | 18.09.16,Краснодар,2,Ростов,1 57 | 19.09.16,Зенит,4,Рубин,1 58 | 24.09.16,ЦСКА,1,Краснодар,1 59 | 24.09.16,Ростов,1,Локомотив,0 60 | 25.09.16,Оренбург,0,Урал,1 61 | 25.09.16,Арсенал,0,Терек,0 62 | 25.09.16,Спартак,0,Уфа,1 63 | 25.09.16,Анжи,2,Зенит,2 64 | 26.09.16,Амкар,0,Крылья Советов,0 65 | 26.09.16,Рубин,2,Томь,1 66 | 01.10.16,Томь,1,Урал,1 67 | 01.10.16,Крылья Советов,2,Анжи,1 68 | 01.10.16,Локомотив,1,Арсенал,1 69 | 01.10.16,Терек,2,Оренбург,1 70 | 02.10.16,Уфа,1,Амкар,1 71 | 02.10.16,Зенит,4,Спартак,2 72 | 02.10.16,Краснодар,1,Рубин,0 73 | 02.10.16,Ростов,2,ЦСКА,0 74 | 14.10.16,ЦСКА,1,Уфа,0 75 | 15.10.16,Амкар,0,Локомотив,0 76 | 15.10.16,Рубин,3,Крылья Советов,0 77 | 15.10.16,Спартак,1,Ростов,0 78 | 16.10.16,Урал,0,Зенит,2 79 | 16.10.16,Оренбург,3,Томь,1 80 | 16.10.16,Арсенал,0,Краснодар,0 81 | 17.10.16,Анжи,0,Терек,0 82 | 21.10.16,Крылья Советов,1,Арсенал,1 83 | 22.10.16,Томь,0,Анжи,3 84 | 22.10.16,Уфа,0,Ростов,0 85 | 22.10.16,Урал,0,Спартак,1 86 | 22.10.16,Терек,3,Рубин,1 87 | 23.10.16,Локомотив,1,ЦСКА,0 88 | 23.10.16,Краснодар,1,Амкар,0 89 | 24.10.16,Зенит,1,Оренбург,0 90 | 29.10.16,Амкар,1,Ростов,0 91 | 29.10.16,Спартак,3,ЦСКА,1 92 | 30.10.16,Урал,1,Терек,4 93 | 30.10.16,Арсенал,0,Уфа,2 94 | 30.10.16,Зенит,1,Томь,0 95 | 30.10.16,Анжи,0,Краснодар,0 96 | 31.10.16,Оренбург,1,Крылья Советов,0 97 | 31.10.16,Рубин,2,Локомотив,0 98 | 05.11.16,Томь,0,Спартак,1 99 | 05.11.16,Уфа,2,Рубин,3 100 | 05.11.16,Крылья Советов,2,Урал,2 101 | 05.11.16,Локомотив,4,Анжи,0 102 | 06.11.16,Терек,2,Зенит,1 103 | 06.11.16,Ростов,4,Арсенал,1 104 | 06.11.16,ЦСКА,2,Амкар,2 105 | 06.11.16,Краснодар,3,Оренбург,3 106 | 18.11.16,Арсенал,0,ЦСКА,1 107 | 18.11.16,Рубин,0,Ростов,0 108 | 19.11.16,Оренбург,1,Локомотив,1 109 | 19.11.16,Анжи,0,Уфа,1 110 | 20.11.16,Краснодар,3,Урал,0 111 | 20.11.16,Спартак,1,Амкар,0 112 | 20.11.16,Зенит,3,Крылья Советов,1 113 | 21.11.16,Терек,0,Томь,0 114 | 25.11.16,Уфа,1,Оренбург,0 115 | 26.11.16,Амкар,1,Арсенал,0 116 | 26.11.16,ЦСКА,0,Рубин,0 117 | 26.11.16,Локомотив,1,Урал,1 118 | 26.11.16,Терек,0,Спартак,1 119 | 27.11.16,Крылья Советов,3,Томь,0 120 | 27.11.16,Ростов,2,Анжи,0 121 | 27.11.16,Краснодар,2,Зенит,1 122 | 30.11.16,Урал,1,Ростов,0 123 | 30.11.16,Рубин,1,Арсенал,0 124 | 30.11.16,ЦСКА,2,Оренбург,0 125 | 30.11.16,Зенит,2,Уфа,0 126 | 01.12.16,Крылья Советов,4,Спартак,0 127 | 01.12.16,Терек,2,Краснодар,1 128 | 01.12.16,Томь,1,Локомотив,6 129 | 01.12.16,Анжи,3,Амкар,1 130 | 03.12.16,ЦСКА,4,Урал,0 131 | 03.12.16,Ростов,0,Зенит,0 132 | 04.12.16,Локомотив,2,Терек,0 133 | 05.12.16,Уфа,1,Томь,0 134 | 05.12.16,Амкар,3,Оренбург,0 135 | 05.12.16,Краснодар,1,Крылья Советов,1 136 | 05.12.16,Спартак,2,Рубин,1 137 | 05.12.16,Арсенал,1,Анжи,0 -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Replay Table 6 | 7 | 8 | 9 | 10 |

Replay Table

11 | 12 |
16 |
17 | 18 |
22 |
23 | 24 |
28 |
29 | 30 |
34 |
35 | 36 |
40 |
41 | 42 | 43 |
50 |
51 | 52 |
58 |
59 | 60 |
66 |
67 | 68 | 69 |
73 |
74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /dist/replay-table.css: -------------------------------------------------------------------------------- 1 | html{font-family:Tahoma,sans-serif}h1{color:green}.replayTable{position:relative;float:left;clear:both;margin-bottom:50px}.replayTable>.controls-container{margin-bottom:30px}.replayTable>.controls-container>.controls.hidden{display:none}.replayTable>.controls-container>.controls>div{display:inline-block;vertical-align:bottom;margin-right:5px;position:relative;cursor:pointer}.replayTable>.controls-container>.controls>div.disabled{opacity:.5;cursor:not-allowed}.replayTable>.controls-container>.controls>.play{width:14px;height:14px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-left:12px solid #000;border-top:7px solid transparent;border-bottom:7px solid transparent;border-right:0 solid transparent;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px}.replayTable>.controls-container>.controls>.pause{width:14px;height:14px}.replayTable>.controls-container>.controls>.pause:before{left:0}.replayTable>.controls-container>.controls>.pause:after,.replayTable>.controls-container>.controls>.pause:before{content:"";display:block;position:absolute;width:5px;height:100%;background:#000;top:0;-webkit-border-radius:2px;-moz-border-radius:2px;border-radius:2px}.replayTable>.controls-container>.controls>.pause:after{right:0}.replayTable>.controls-container>.controls>.replay{width:10px;height:10px;border:2px solid #000;-webkit-border-radius:20px;-moz-border-radius:20px;border-radius:20px}.replayTable>.controls-container>.controls>.replay:before{width:8px;height:8px;background:#fff;right:-6px;top:4px}.replayTable>.controls-container>.controls>.replay:after,.replayTable>.controls-container>.controls>.replay:before{content:"";display:block;position:absolute;-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg)}.replayTable>.controls-container>.controls>.replay:after{border:4px solid transparent;border-top-color:#000;right:-7px;top:1px}.replayTable>.controls-container>.controls>.next,.replayTable>.controls-container>.controls>.previous{width:14px;height:14px}.replayTable>.controls-container>.controls>.next:before,.replayTable>.controls-container>.controls>.previous:before{content:"";position:absolute;display:block;top:2px;width:8px;height:8px;border-right:2px solid #000;border-top:2px solid #000}.replayTable>.controls-container>.controls>.previous:before{left:5px;-webkit-transform:rotate(-135deg);-moz-transform:rotate(-135deg);-ms-transform:rotate(-135deg);-o-transform:rotate(-135deg);transform:rotate(-135deg)}.replayTable>.controls-container>.controls>.next:before{right:5px;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.replayTable .slider{display:inline-block;margin-left:20px;width:230px;position:relative;height:1px;background:rgba(0,0,0,.1)}.replayTable .slider .slider-toggle{position:absolute;display:block;width:auto;white-space:nowrap;padding:0 2px;min-width:12px;height:16px;background:#fff;border:1px solid #999;top:-24px;cursor:pointer;border-radius:3px;text-align:center;line-height:17px;font-size:10px;color:#000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.replayTable .slider .slider-toggle:before{bottom:-5px;border-color:#fff transparent;z-index:2}.replayTable .slider .slider-toggle:after,.replayTable .slider .slider-toggle:before{content:"";position:absolute;left:50%;margin-left:-4px;border-width:6px 4px 0;border-style:solid;display:block;width:0}.replayTable .slider .slider-toggle:after{bottom:-7px;border-color:#999 transparent}.replayTable .slider .slider-available{position:absolute;display:block;height:100%;left:0;top:0;background:rgba(0,0,0,.1)}.replayTable .slider .slider-progress{position:absolute;display:block;height:100%;left:0;top:0;background:rgba(0,0,0,.3)}.drilldown-contorls .back{display:inline-block;vertical-align:middle;position:relative;width:14px;height:14px;margin-right:10px;font-size:0;cursor:pointer}.drilldown-contorls .back:before{content:"";position:absolute;display:block;top:2px;left:5px;width:8px;height:8px;border-right:2px solid #000;border-top:2px solid #000;-webkit-transform:rotate(-135deg);-moz-transform:rotate(-135deg);-ms-transform:rotate(-135deg);-o-transform:rotate(-135deg);transform:rotate(-135deg)}.drilldown-contorls .item{display:inline-block;vertical-align:bottom;font-size:14px;text-transform:uppercase;font-weight:800}.replayTable .table-container{position:relative}.replayTable .table-container table{position:relative;left:0;top:0;border-spacing:0}.replayTable .table-container table+table{position:absolute;left:0;top:0}.replayTable .table-container table.drilldown{position:relative}.replayTable .hidden{visibility:hidden}.replayTable .table-container.classic.drilldowned .hidden{display:none}.replayTable th{text-align:left;text-transform:uppercase;font-size:12px;padding-bottom:5px;border-bottom:1px dotted rgba(0,0,0,.1)}.replayTable td{white-space:nowrap;padding:0 5px;font-size:14px;line-height:14px;min-height:23px;height:23px}.replayTable td.outcome{width:5px;padding:0;min-height:23px}.replayTable .clickable{cursor:pointer}.replayTable .clickable:hover{text-decoration:underline}.replayTable .calculation{text-align:center}.replayTable .controls-container.sparklines{margin-bottom:10px}.replayTable .table-container.sparklines:after{content:"";display:block;clear:both}.replayTable .table-container.sparklines>table{float:left;position:relative}.replayTable .table-container.sparklines>table td{border-bottom:2px solid #fff}.replayTable .table-container.sparklines .slider-cell{height:0}.replayTable td.spark{width:5px!important;max-width:5px;height:17px;padding:3px 0;position:relative;white-space:nowrap;font-size:14px;min-height:23px}.replayTable td.spark .spark-position{position:absolute;display:block;width:100%;height:1px!important;line-height:1px;font-size:1px;background:#000;left:0}.replayTable td.spark.muted .spark-position{opacity:.4}.replayTable td.change{color:rgba(0,0,0,.8);font-size:14px}.replayTable .main.right td.change{text-align:center}.replayTable .main.right td.label.change,.replayTable .main.right td.opponent.change{text-align:left}.replayTable td.spark.muted{background-color:transparent!important}.replayTable td.spark.overlapped{-webkit-filter:grayscale(100%);-moz-filter:grayscale(100%);-ms-filter:grayscale(100%);-o-filter:grayscale(100%);filter:grayscale(100%);filter:gray}.replayTable span.spark-score{position:absolute;left:7px;top:0;line-height:23px}.replayTable .sparklines tr.muted,.replayTable span.spark-score.muted{visibility:hidden}.replayTable .sparklines tr.muted>td.opponent{font-size:0}.replayTable .main.right{z-index:2;background:hsla(0,0%,100%,.3)}.controls-container.sparklines .drilldown-control{display:inline-block;vertical-align:bottom;margin-left:10px;line-height:13px;font-size:14px;text-transform:uppercase;font-weight:800}.replayTable .sparklines-slider .slider-cell{position:relative}.replayTable .sparklines-slider.top .slider-toggle{position:absolute;display:block;width:auto;white-space:nowrap;padding:0 2px;min-width:12px;height:16px;background:#fff;border:1px solid #999;top:-23px;margin-left:-6px;cursor:pointer;border-radius:3px;text-align:center;line-height:17px;font-size:10px;color:#000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.replayTable .sparklines-slider.top .slider-toggle:before{bottom:-5px;border-color:#fff transparent;z-index:2}.replayTable .sparklines-slider.top .slider-toggle:after,.replayTable .sparklines-slider.top .slider-toggle:before{content:"";position:absolute;left:50%;margin-left:-4px;border-width:6px 4px 0;border-style:solid;display:block;width:0}.replayTable .sparklines-slider.top .slider-toggle:after{bottom:-7px;border-color:#999 transparent}.replayTable .sparklines-slider.bottom .slider-toggle{position:absolute;display:block;width:auto;white-space:nowrap;padding:0 2px;min-width:12px;height:16px;background:#fff;border:1px solid #999;bottom:-23px;margin-left:-6px;cursor:pointer;border-radius:3px;text-align:center;line-height:17px;font-size:10px;color:#000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.replayTable .sparklines-slider.bottom .slider-toggle:before{content:"";position:absolute;top:-6px;left:50%;margin-left:-4px;border-width:0 4px 6px;border-style:solid;border-color:#fff transparent;display:block;width:0;z-index:2}.replayTable .sparklines-slider.bottom .slider-toggle:after{content:"";position:absolute;top:-7px;left:50%;margin-left:-4px;border-width:0 4px 6px;border-style:solid;border-color:#999 transparent;display:block;width:0} -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Replay Table 6 | 7 | 8 | 9 | 10 |

Replay Table

11 | 12 |
16 |
17 | 18 |
22 |
23 | 24 |
28 |
29 | 30 |
34 |
35 | 36 |
40 |
41 | 42 | 43 |
50 |
51 | 52 |
58 |
59 | 60 |
66 |
67 | 68 | 69 |
73 |
74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replay-table", 3 | "version": "1.0.6", 4 | "description": "Visualize sport seasons with interactive standings", 5 | "keywords": [ 6 | "visualization", 7 | "sport", 8 | "season", 9 | "standings", 10 | "replay", 11 | "table", 12 | "d3" 13 | ], 14 | "homepage": "https://antoniokov.com/replay", 15 | "bugs": "https://github.com/antoniokov/replay-table/issues", 16 | "license": "MIT", 17 | "author": { 18 | "name": "Anton Iokov", 19 | "url": "https://github.com/antoniokov" 20 | }, 21 | "contributors": [ 22 | { 23 | "name": "Anton Iokov", 24 | "url": "https://github.com/antoniokov" 25 | }, 26 | { 27 | "name": "Daria Krupenkina", 28 | "url": "https://github.com/dariak" 29 | } 30 | ], 31 | "main": "dist/replay-table.js", 32 | "unpkg": "dist/replay-table.min.js", 33 | "module": "src/replay-table.js", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/antoniokov/replay-table" 37 | }, 38 | "scripts": { 39 | "build": "webpack -p", 40 | "start": "webpack-dev-server --port 8003" 41 | }, 42 | "devDependencies": { 43 | "babel-cli": "^6.24.0", 44 | "babel-loader": "^6.4.1", 45 | "babel-preset-es2015": "^6.24.0", 46 | "css-loader": "^0.27.3", 47 | "extract-text-webpack-plugin": "^2.1.0", 48 | "html-webpack-plugin": "^2.28.0", 49 | "style-loader": "^0.16.0", 50 | "unminified-webpack-plugin": "^1.2.0", 51 | "webpack": "^2.3.1", 52 | "webpack-dev-server": "^2.4.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/calculate/calculate.js: -------------------------------------------------------------------------------- 1 | import * as calculators from './calculators'; 2 | import parametrize from '../configure/parametrize'; 3 | import config from './config'; 4 | 5 | 6 | export default function (transformedData, userConfig) { 7 | const params = parametrize(config, userConfig); 8 | 9 | const enriched = calculators.enrich(transformedData, params); 10 | const sorted = calculators.sort(enriched, params); 11 | const positioned = calculators.position(sorted, params); 12 | 13 | return calculators.addMeta(positioned, params); 14 | }; 15 | -------------------------------------------------------------------------------- /src/calculate/calculations.js: -------------------------------------------------------------------------------- 1 | const checkingFunctions = { 2 | alwaysTrue: transformedData => true, 3 | 4 | hasOutcome: outcome => transformedData => 5 | transformedData.some(round => round.results.some(result => result.outcome === outcome)), 6 | 7 | hasMatches: transformedData => 8 | transformedData.some(round => round.results.some(result => result.match)) 9 | }; 10 | 11 | 12 | export default { 13 | 'points': { 14 | check: checkingFunctions.alwaysTrue, 15 | calculate: result => result.change || 0 16 | }, 17 | 'rounds': { 18 | check: checkingFunctions.alwaysTrue, 19 | calculate: result => result.change === null ? 0 : 1 20 | }, 21 | 22 | 23 | 'wins': { 24 | check: checkingFunctions.hasOutcome('win'), 25 | calculate: result => result.outcome === 'win' ? 1 : 0 26 | }, 27 | 'losses': { 28 | check: checkingFunctions.hasOutcome('loss'), 29 | calculate: result => result.outcome === 'loss' ? 1 : 0 30 | }, 31 | 'draws': { 32 | check: checkingFunctions.hasOutcome('draw'), 33 | calculate: result => result.outcome === 'draw' ? 1 : 0 34 | }, 35 | 36 | 37 | 'goalsFor': { 38 | check: checkingFunctions.hasMatches, 39 | calculate: result => result.match ? result.match.score : 0 40 | }, 41 | 'goalsAgainst': { 42 | check: checkingFunctions.hasMatches, 43 | calculate: result => result.match ? result.match.opponentScore : 0 44 | }, 45 | 46 | 'goalsDifference': { 47 | check: checkingFunctions.hasMatches, 48 | calculate: result => result.match ? result.match.score - result.match.opponentScore : 0 49 | }, 50 | 51 | 52 | 'winningPercentage': { 53 | check: checkingFunctions.hasOutcome('win'), 54 | calculate: calculatedResult => calculatedResult.rounds.total ? calculatedResult.wins.total / calculatedResult.rounds.total : 0, 55 | isPost: true 56 | } 57 | }; -------------------------------------------------------------------------------- /src/calculate/calculators/add-meta.js: -------------------------------------------------------------------------------- 1 | export default function (data, params) { 2 | const enriched = data.map((round, i) => { 3 | return { 4 | meta: { 5 | name: round.name, 6 | index: i, 7 | isLast: false, 8 | items: round.results.filter(result => result.change !== null).length, 9 | hasOnlyOutcomes: round.results.every(result => result.outcome || result.change === null), 10 | biggestChange: Math.max(...round.results.map(result => Math.abs(result.change || 0))), 11 | sumOfChanges: round.results.reduce((sum, result) => sum + (result.change || 0), 0), 12 | }, 13 | results: round.results 14 | } 15 | }); 16 | 17 | const lastRound = enriched 18 | .filter(round => round.results.some(result => result.change !== null)) 19 | .reduce((maxIndex, round) => Math.max(round.meta.index, maxIndex), 0); 20 | 21 | enriched[lastRound].meta.isLast = true; 22 | 23 | return { 24 | meta: { 25 | lastRound: lastRound, 26 | }, 27 | results: enriched 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/calculate/calculators/enrich.js: -------------------------------------------------------------------------------- 1 | import calculatables from '../calculations'; 2 | import getItems from '../../helpers/data/get-items'; 3 | 4 | 5 | export default function (transformedData, params) { 6 | const calculations = Object.keys(calculatables) 7 | .filter(calc => calculatables[calc].check(transformedData)); 8 | 9 | const items = getItems(transformedData); 10 | 11 | const initialStats = calculations.reduce((obj, calc) => Object.assign(obj, { [calc]: 0 }), {}); 12 | const itemStats = items.reduce((obj, item) => Object.assign(obj, { [item]: Object.assign({}, initialStats) }), {}); 13 | 14 | 15 | return transformedData.map(round => { 16 | const results = round.results.map(result => { 17 | const calculatedResult = Object.assign({}, result); 18 | const stats = itemStats[result.item]; 19 | 20 | calculations.filter(calc => !calculatables[calc].isPost) 21 | .forEach(calc => { 22 | const change = calculatables[calc].calculate(calculatedResult); 23 | calculatedResult[calc] = { 24 | change: change, 25 | total: stats[calc] + change 26 | }; 27 | stats[calc] += change; 28 | }); 29 | 30 | calculations.filter(calc => calculatables[calc].isPost) 31 | .forEach(calc => { 32 | const total = calculatables[calc].calculate(calculatedResult); 33 | calculatedResult[calc] = { 34 | change: null, 35 | total: total 36 | } 37 | }); 38 | 39 | return calculatedResult; 40 | }); 41 | 42 | return { 43 | name: round.name, 44 | results: results 45 | } 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/calculate/calculators/index.js: -------------------------------------------------------------------------------- 1 | export {default as enrich} from './enrich'; 2 | export {default as sort} from './sort'; 3 | export {default as position} from './position'; 4 | export {default as addMeta} from './add-meta'; 5 | -------------------------------------------------------------------------------- /src/calculate/calculators/position.js: -------------------------------------------------------------------------------- 1 | import getCompareFunction from '../helpers/get-compare-function'; 2 | 3 | 4 | export default function (data, params) { 5 | const compare = getCompareFunction(params.orderBy); 6 | 7 | return data.map(round => { 8 | const results = round.results.map((result, i) => { 9 | const positionedResult = Object.assign({}, result); 10 | 11 | const itemsHigher = round.results.filter(res => compare(res, result) < 0); 12 | const itemsEqual = round.results.filter(res => res.item !== result.item && compare(res, result) === 0); 13 | 14 | positionedResult.position = { 15 | strict: i + 1, 16 | highest: itemsHigher.length + 1, 17 | lowest: itemsHigher.length + itemsEqual.length + 1 18 | }; 19 | 20 | return positionedResult; 21 | }); 22 | 23 | return { 24 | name: round.name, 25 | results: results 26 | } 27 | }) 28 | }; 29 | -------------------------------------------------------------------------------- /src/calculate/calculators/sort.js: -------------------------------------------------------------------------------- 1 | import sortRoundResults from '../helpers/sort-round-results'; 2 | import getCompareFunction from '../helpers/get-compare-function'; 3 | 4 | 5 | export default function (data, params) { 6 | const sorted = data.slice(0, 1); 7 | 8 | const compareFunction = getCompareFunction(params.orderBy); 9 | data.slice(1).forEach((round, i) => { 10 | sorted.push({ 11 | name: round.name, 12 | results: sortRoundResults(round.results, sorted[i].results, compareFunction) 13 | }); 14 | }); 15 | 16 | return sorted; 17 | }; 18 | -------------------------------------------------------------------------------- /src/calculate/config.js: -------------------------------------------------------------------------------- 1 | import calculations from './calculations'; 2 | import validateArray from '../helpers/validation/validate-array'; 3 | import isString from '../helpers/general/is-string'; 4 | 5 | 6 | export default { 7 | id: { 8 | default: '', 9 | parse: input => input, 10 | validate: isString 11 | }, 12 | 13 | orderBy: { 14 | default: ['points'], 15 | parse: input => input.split(','), 16 | validate: value => validateArray(value, value => calculations.hasOwnProperty(value)) 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/calculate/helpers/get-compare-function.js: -------------------------------------------------------------------------------- 1 | export default function (orderBy) { 2 | return (a,b) => { 3 | const tieBreaker = orderBy.filter(calc => b[calc].total !== a[calc].total)[0]; 4 | 5 | if (tieBreaker) { 6 | return b[tieBreaker].total - a[tieBreaker].total; 7 | } else { 8 | return 0; 9 | } 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/calculate/helpers/sort-round-results.js: -------------------------------------------------------------------------------- 1 | export default function (roundResults, sortedPreviousRoundResults, compareFunction) { 2 | return roundResults.map(result => { 3 | return { 4 | obj: result, 5 | idx: sortedPreviousRoundResults.map(result => result.item).indexOf(result.item), 6 | } 7 | }) 8 | .sort((a,b) => compareFunction(a.obj, b.obj) ? compareFunction(a.obj, b.obj) : a.idx - b.idx) 9 | .map(item => item.obj); 10 | }; 11 | -------------------------------------------------------------------------------- /src/configure/configs/index.js: -------------------------------------------------------------------------------- 1 | export {default as extract} from '../../extract/config'; 2 | export {default as transform} from '../../transform/config'; 3 | export {default as calculate} from '../../calculate/config'; 4 | export {default as visualize} from '../../visualize/config'; 5 | -------------------------------------------------------------------------------- /src/configure/configure.js: -------------------------------------------------------------------------------- 1 | import * as configs from './configs'; 2 | import getPresetConfig from './helpers/get-preset-config'; 3 | import getEmptyConfig from './helpers/get-empty-config'; 4 | import extendConfigs from './helpers/extend-configs'; 5 | import mapParamToModule from './helpers/map-param-to-module'; 6 | import addId from './helpers/add-id'; 7 | import toCamelCase from '../helpers/general/to-camel-case'; 8 | import warn from '../helpers/warn'; 9 | 10 | 11 | const reservedKeywords = ['preset']; 12 | 13 | export default function (id, userConfig) { 14 | const config = getPresetConfig(userConfig.preset) || getEmptyConfig(configs); 15 | const extendedConfigs = extendConfigs(configs, config, userConfig); 16 | 17 | Object.keys(userConfig) 18 | .filter(param => !reservedKeywords.includes(param)) 19 | .map(param => toCamelCase(param)) 20 | .forEach(param => { 21 | const module = mapParamToModule(param, extendedConfigs); 22 | 23 | if (module) { 24 | config[module][param] = extendedConfigs[module][param].parse(userConfig[param]); 25 | } else { 26 | warn(`Sorry, there is no "${param}" parameter available. Ignoring it and moving on.`); 27 | } 28 | }); 29 | 30 | return addId(id, config); 31 | }; 32 | -------------------------------------------------------------------------------- /src/configure/extensions/index.js: -------------------------------------------------------------------------------- 1 | import * as transformConfigs from '../../transform/configs'; 2 | import * as visualizeConfigs from '../../visualize/configs'; 3 | 4 | export const transform = { 5 | processorField: 'transformer', 6 | configs: transformConfigs 7 | }; 8 | 9 | export const visualize = { 10 | processorField: 'visualizer', 11 | configs: visualizeConfigs 12 | }; 13 | -------------------------------------------------------------------------------- /src/configure/helpers/add-id.js: -------------------------------------------------------------------------------- 1 | export default function (id, config) { 2 | return Object.keys(config).reduce((obj, module) => { 3 | const moduleConfig = Object.assign({ id: id }, config[module]); 4 | return Object.assign(obj, { [module]: moduleConfig }); 5 | }, {}); 6 | }; 7 | -------------------------------------------------------------------------------- /src/configure/helpers/extend-configs.js: -------------------------------------------------------------------------------- 1 | import * as extensions from '../extensions'; 2 | 3 | 4 | export default function (configs, presetConfig, userConfig) { 5 | return Object.keys(configs).map(module => { 6 | if (!extensions.hasOwnProperty(module)) { 7 | return { 8 | name: module, 9 | config: configs[module] 10 | }; 11 | } 12 | 13 | const processorField = extensions[module].processorField; 14 | const processor = configs[module][processorField].validate(userConfig[processorField]) 15 | ? userConfig[processorField] 16 | : presetConfig[module][processorField] || configs[module][processorField].default; 17 | 18 | const config = extensions[module].configs[processor] 19 | ? Object.assign({}, configs[module], extensions[module].configs[processor]) 20 | : configs[module]; 21 | 22 | return { 23 | name: module, 24 | config: config 25 | }; 26 | }).reduce((extendedConfigs, elem) => Object.assign(extendedConfigs, { [elem.name]: elem.config }), {}); 27 | }; 28 | -------------------------------------------------------------------------------- /src/configure/helpers/get-empty-config.js: -------------------------------------------------------------------------------- 1 | export default function (configs) { 2 | return Object.keys(configs).reduce((obj, module) => Object.assign(obj, { [module]: {} }), {}); 3 | }; 4 | -------------------------------------------------------------------------------- /src/configure/helpers/get-preset-config.js: -------------------------------------------------------------------------------- 1 | import * as presets from '../presets'; 2 | import warn from '../../helpers/warn'; 3 | 4 | 5 | export default function (userPreset) { 6 | if (!userPreset) { 7 | return null; 8 | } 9 | 10 | if (!presets.hasOwnProperty(userPreset)) { 11 | warn(`No "${userPreset}" preset for now, sorry about that. Moving on with the default settings.`); 12 | return null; 13 | } 14 | 15 | return Object.keys(presets[userPreset]) 16 | .reduce((obj, key) => Object.assign(obj, { [key]: Object.assign({}, presets[userPreset][key]) }) , {}); 17 | }; 18 | -------------------------------------------------------------------------------- /src/configure/helpers/map-param-to-module.js: -------------------------------------------------------------------------------- 1 | export default function (param, configs) { 2 | const modules = Object.keys(configs) 3 | .filter(config => configs[config].hasOwnProperty(param)); 4 | return modules.length > 0 ? modules[0] : null; 5 | }; 6 | -------------------------------------------------------------------------------- /src/configure/initialize.js: -------------------------------------------------------------------------------- 1 | import parametrize from './parametrize'; 2 | 3 | 4 | export default function (processorField, moduleConfig, processorsConfigs, userConfig) { 5 | const processor = parametrize({ [processorField]: moduleConfig[processorField] }, 6 | { [processorField]: userConfig[processorField]})[processorField]; 7 | 8 | 9 | const config = processorsConfigs.hasOwnProperty(processor) 10 | ? Object.assign({}, moduleConfig, processorsConfigs[processor]) 11 | : moduleConfig; 12 | 13 | return parametrize(config, userConfig); 14 | }; 15 | -------------------------------------------------------------------------------- /src/configure/parametrize.js: -------------------------------------------------------------------------------- 1 | import warn from '../helpers/warn'; 2 | 3 | 4 | export default function (config, userConfig) { 5 | Object.keys(userConfig).filter(param => !config.hasOwnProperty(param)) 6 | .forEach(param => warn(`Sorry, there is no "${param}" parameter available. Ignoring it and moving on.`)); 7 | 8 | const params = Object.keys(config).reduce((obj, param) => Object.assign(obj, { [param]: config[param].default }), {}); 9 | 10 | Object.keys(userConfig) 11 | .filter(param => config.hasOwnProperty(param)) 12 | .forEach(param => { 13 | if (config[param].validate(userConfig[param])) { 14 | params[param] = userConfig[param]; 15 | } else if (userConfig[param] !== undefined) { 16 | warn(`Sorry, we cannot accept ${userConfig[param]} as ${param}. \ 17 | Moving on with the default value, which is ${params[param]}.`); 18 | } 19 | }); 20 | 21 | return params; 22 | }; 23 | -------------------------------------------------------------------------------- /src/configure/presets/chgk.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extract: {}, 3 | transform: { 4 | transformer: 'pointsTable', 5 | changeToOutcome: { 6 | 1: 'win', 7 | 0: 'loss' 8 | } 9 | }, 10 | calculate: { 11 | orderBy: ['points'] 12 | }, 13 | visualize: { 14 | columns: ['position', 'item', 'points', 'outcome'], 15 | labels: ['#', 'Команда', 'Взятых'], 16 | positionWhenTied: 'range' 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/configure/presets/f1.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extract: {}, 3 | transform: { 4 | transformer: 'pointsTable', 5 | changeToOutcome: { 6 | 25: 'win' 7 | }, 8 | insertStartRound: 'Start →' 9 | }, 10 | calculate: { 11 | orderBy: ['points', 'wins'] 12 | }, 13 | visualize: { 14 | columns: ['position', 'item', 'points', 'points.change'], 15 | labels: ['#', 'Driver', 'Points'] 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/configure/presets/index.js: -------------------------------------------------------------------------------- 1 | export {default as winLoss} from './win-loss'; 2 | export {default as chgk} from './chgk'; 3 | export {default as f1} from './f1'; 4 | export {default as matches} from './matches'; 5 | -------------------------------------------------------------------------------- /src/configure/presets/matches.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extract: {}, 3 | transform: { 4 | transformer: 'listOfMatches', 5 | collapseToRounds: false 6 | }, 7 | calculate: { 8 | orderBy: ['points', 'goalsDifference', 'goalsFor'] 9 | }, 10 | visualize: { 11 | columns: ['position', 'item', 'points', 'outcome', 'match'], 12 | labels: ['#', 'Team', 'Points'] 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/configure/presets/win-loss.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extract: {}, 3 | transform: { 4 | transformer: 'listOfMatches', 5 | changeToOutcome: { 6 | 1: 'win', 7 | 0: 'loss' 8 | } 9 | }, 10 | calculate: { 11 | orderBy: ['winningPercentage', 'wins'] 12 | }, 13 | visualize: { 14 | visualizer: 'classic', 15 | columns: ['position', 'item', 'rounds', 'wins', 'losses', 'winningPercentage', 'outcome', 'match'], 16 | labels: ['#', 'Team', 'G', 'W' , 'L', 'Win %'] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/extract/config.js: -------------------------------------------------------------------------------- 1 | import * as extractors from './extractors'; 2 | import isString from '../helpers/general/is-string'; 3 | 4 | 5 | export default { 6 | id: { 7 | default: '', 8 | parse: input => input, 9 | validate: isString 10 | }, 11 | 12 | extractor: { 13 | default: 'csv', 14 | parse: input => input, 15 | validate: value => extractors.hasOwnProperty(value) 16 | }, 17 | 18 | source: { 19 | default: undefined, 20 | parse: input => input, 21 | validate: isString 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/extract/extract.js: -------------------------------------------------------------------------------- 1 | import * as extractors from './extractors'; 2 | import guessExtractor from './helpers/guess-extractor'; 3 | import config from './config'; 4 | import crash from '../helpers/crash'; 5 | import warn from '../helpers/warn'; 6 | 7 | 8 | export default function (userConfig) { 9 | const source = config.source.validate(userConfig.source) && userConfig.source; 10 | 11 | if(!source) { 12 | crash(`Please, check the data source. We couldn't get anything out from ${userConfig.source}`); 13 | return; 14 | } 15 | 16 | const extractor = userConfig.extractor || guessExtractor(source); 17 | 18 | if (!config.extractor.validate(extractor)) { 19 | warn(`We couldn't determine the extractor so we'll try to use the default one, which is ${config.extractor.default}`); 20 | return extractors[config.extractor.default](source); 21 | } 22 | 23 | return extractors[extractor](source); 24 | }; 25 | -------------------------------------------------------------------------------- /src/extract/extractors/csv.js: -------------------------------------------------------------------------------- 1 | export default function (path) { 2 | return new Promise ((resolve, reject) => { 3 | d3.text(path, text => { 4 | if (!text) { 5 | reject(`Sorry, we can't reach your csv file`); 6 | return; 7 | } 8 | 9 | const parsed = d3.csvParseRows(text); 10 | resolve(parsed); 11 | }); 12 | } 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/extract/extractors/index.js: -------------------------------------------------------------------------------- 1 | export {default as csv} from './csv' 2 | export {default as json} from './json' 3 | -------------------------------------------------------------------------------- /src/extract/extractors/json.js: -------------------------------------------------------------------------------- 1 | export default function (path) { 2 | return new Promise ((resolve, reject) => { 3 | d3.json(path, data => { 4 | if (!data) { 5 | reject(`Sorry, we can't reach your json file`); 6 | return; 7 | } 8 | 9 | resolve(data); 10 | }); 11 | } 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/extract/helpers/guess-extractor.js: -------------------------------------------------------------------------------- 1 | import * as extractors from '../extractors'; 2 | import getFileExtension from '../../helpers/general/get-file-extension'; 3 | 4 | 5 | export default function (source) { 6 | const extension = getFileExtension(source); 7 | return extractors.hasOwnProperty(extension) ? extension : null; 8 | }; 9 | -------------------------------------------------------------------------------- /src/helpers/crash.js: -------------------------------------------------------------------------------- 1 | export default function (text) { 2 | console.log(text); 3 | } -------------------------------------------------------------------------------- /src/helpers/data/get-item-results.js: -------------------------------------------------------------------------------- 1 | export default function (results, item, filter = false) { 2 | return results 3 | .map(round => { 4 | const result = round.results.filter(result => result.item === item)[0]; 5 | return Object.assign({}, result, { roundMeta: round.meta }); 6 | }).filter(result => !filter || result.change !== null); 7 | }; 8 | -------------------------------------------------------------------------------- /src/helpers/data/get-items.js: -------------------------------------------------------------------------------- 1 | export default function (results) { 2 | return [...new Set(results.reduce((list, round) => [...list, ...round.results.map(result => result.item)], []))]; 3 | }; 4 | -------------------------------------------------------------------------------- /src/helpers/general/flip-object.js: -------------------------------------------------------------------------------- 1 | export default function (obj) { 2 | return Object.keys(obj).reduce((result, key) => { 3 | const keyNumber = Number.parseInt(key, 10); 4 | const newValue = isNaN(keyNumber) ? key : keyNumber; 5 | const newKey = obj[key]; 6 | return Object.assign(result, { [newKey]: newValue }) 7 | }, {}); 8 | } -------------------------------------------------------------------------------- /src/helpers/general/from-camel-case.js: -------------------------------------------------------------------------------- 1 | export default function (str) { 2 | return str.replace(/([A-Z])/g, ' $1') 3 | .replace(/^./, str => str.toUpperCase()); 4 | }; 5 | -------------------------------------------------------------------------------- /src/helpers/general/get-file-extension.js: -------------------------------------------------------------------------------- 1 | export default function (filename) { 2 | return filename.slice((Math.max(0, filename.lastIndexOf(".")) || Infinity) + 1); 3 | }; 4 | -------------------------------------------------------------------------------- /src/helpers/general/is-between.js: -------------------------------------------------------------------------------- 1 | export default function (n, a, b) { 2 | return (n - a) * (n - b) <= 0 3 | }; 4 | -------------------------------------------------------------------------------- /src/helpers/general/is-string.js: -------------------------------------------------------------------------------- 1 | export default function (value) { 2 | return typeof value === 'string' || value instanceof String; 3 | } 4 | -------------------------------------------------------------------------------- /src/helpers/general/json-parse.js: -------------------------------------------------------------------------------- 1 | export default function(input) { 2 | try { 3 | return JSON.parse(input.replace(/'/g, '"')); 4 | } catch(e) { 5 | return null; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/helpers/general/number-to-change.js: -------------------------------------------------------------------------------- 1 | export default function (number, zeroString = '') { 2 | if (number > 0) { 3 | return `+${number}`; 4 | } else if (number < 0) { 5 | return number.toString(); 6 | } else { 7 | return zeroString; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/helpers/general/to-camel-case.js: -------------------------------------------------------------------------------- 1 | export default function (str) { 2 | return str.replace(/-([a-z])/g, g => g[1].toUpperCase()); 3 | } 4 | -------------------------------------------------------------------------------- /src/helpers/general/to-css.js: -------------------------------------------------------------------------------- 1 | export default function (str) { 2 | return str.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase(); 3 | }; 4 | -------------------------------------------------------------------------------- /src/helpers/general/transpose.js: -------------------------------------------------------------------------------- 1 | export default function(matrix) { 2 | return Object.keys(matrix[0]) 3 | .map(colNumber => matrix.map(rowNumber => rowNumber[colNumber])); 4 | } 5 | -------------------------------------------------------------------------------- /src/helpers/parsing/parse-object.js: -------------------------------------------------------------------------------- 1 | export default function(input) { 2 | try { 3 | return JSON.parse(input.replace(/'/g, '"')); 4 | } catch(e) { 5 | return null; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/helpers/validation/validate-array.js: -------------------------------------------------------------------------------- 1 | export default function (arr, validateElement = (elem => true)) { 2 | if (!Array.isArray(arr)) { 3 | return false; 4 | } 5 | 6 | return arr.every(elem => validateElement(elem)); 7 | }; 8 | -------------------------------------------------------------------------------- /src/helpers/validation/validate-object.js: -------------------------------------------------------------------------------- 1 | export default function (obj, validateKey = (key => true), validateValue = (value => true)) { 2 | if (!obj || typeof obj !== 'object') { 3 | return false; 4 | } 5 | 6 | const areKeysValid = Object.keys(obj).every(key => validateKey(key)); 7 | const areValuesValid = Object.values(obj).every(value => validateValue(value)); 8 | 9 | return areKeysValid && areValuesValid; 10 | }; 11 | -------------------------------------------------------------------------------- /src/helpers/warn.js: -------------------------------------------------------------------------------- 1 | export default function (text) { 2 | console.log(text); 3 | } -------------------------------------------------------------------------------- /src/magic.js: -------------------------------------------------------------------------------- 1 | import configure from './configure/configure'; 2 | import extract from './extract/extract'; 3 | import transform from './transform/transform'; 4 | import calculate from './calculate/calculate'; 5 | import visualize from './visualize/visualize'; 6 | 7 | import crash from './helpers/crash'; 8 | 9 | 10 | export default function () { 11 | return Array.from(document.getElementsByClassName('replayTable')) 12 | .map(table => { 13 | const config = configure(table.id, table.dataset); 14 | 15 | return Promise.resolve(extract(config.extract)) 16 | .then(raw => { 17 | const transformed = transform(raw, config.transform); 18 | const calculated = calculate(transformed, config.calculate); 19 | return visualize(calculated, config.visualize); 20 | }) 21 | .catch(error => crash(error)); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/replay-table.css: -------------------------------------------------------------------------------- 1 | html{ 2 | font-family: 'Tahoma', sans-serif; 3 | } 4 | 5 | 6 | h1 { 7 | color: green; 8 | } 9 | 10 | 11 | .replayTable{ 12 | position: relative; 13 | 14 | float: left; 15 | clear: both; 16 | 17 | margin-bottom: 50px; 18 | } 19 | 20 | 21 | 22 | 23 | 24 | /* CONTROLS ---------------------------------------------------------------------- 25 | ----------------------------------------------------------------------------------------- */ 26 | 27 | .replayTable > .controls-container{ 28 | margin-bottom: 30px; 29 | } 30 | .replayTable > .controls-container > .controls{} 31 | .replayTable > .controls-container > .controls.hidden{ 32 | display: none; 33 | } 34 | .replayTable > .controls-container > .controls > div{ 35 | display: inline-block; 36 | vertical-align: bottom; 37 | margin-right: 5px; 38 | position: relative; 39 | cursor: pointer; 40 | } 41 | .replayTable > .controls-container > .controls > div.disabled{ 42 | opacity: 0.5; 43 | cursor: not-allowed; 44 | } 45 | .replayTable > .controls-container > .controls > .play { 46 | width: 14px; 47 | height: 14px; 48 | -webkit-box-sizing: border-box; 49 | -moz-box-sizing: border-box; 50 | box-sizing: border-box; 51 | border-left: 12px solid #000; 52 | border-top: 7px solid transparent; 53 | border-bottom: 7px solid transparent; 54 | border-right: 0 solid transparent; 55 | -webkit-border-radius: 2px; 56 | -moz-border-radius: 2px; 57 | border-radius: 2px; 58 | } 59 | 60 | .replayTable > .controls-container > .controls > .pause { 61 | width: 14px; 62 | height: 14px; 63 | } 64 | .replayTable > .controls-container > .controls > .pause:before{ 65 | content: ''; 66 | display: block; 67 | position: absolute; 68 | width: 5px; 69 | height: 100%; 70 | background: #000; 71 | left: 0; 72 | top: 0; 73 | -webkit-border-radius: 2px; 74 | -moz-border-radius: 2px; 75 | border-radius: 2px; 76 | } 77 | .replayTable > .controls-container > .controls > .pause:after{ 78 | content: ''; 79 | display: block; 80 | position: absolute; 81 | width: 5px; 82 | height: 100%; 83 | background: #000; 84 | right: 0; 85 | top: 0; 86 | -webkit-border-radius: 2px; 87 | -moz-border-radius: 2px; 88 | border-radius: 2px; 89 | } 90 | 91 | .replayTable > .controls-container > .controls > .replay { 92 | width: 10px; 93 | height: 10px; 94 | border: 2px solid #000; 95 | 96 | -webkit-border-radius: 20px; 97 | -moz-border-radius: 20px; 98 | border-radius: 20px; 99 | } 100 | .replayTable > .controls-container > .controls > .replay:before{ 101 | content: ''; 102 | display: block; 103 | position: absolute; 104 | width: 8px; 105 | height: 8px; 106 | background: #fff; 107 | right: -6px; 108 | top: 4px; 109 | -webkit-transform: rotate(-45deg); 110 | -moz-transform: rotate(-45deg); 111 | -ms-transform: rotate(-45deg); 112 | -o-transform: rotate(-45deg); 113 | transform: rotate(-45deg); 114 | } 115 | .replayTable > .controls-container > .controls > .replay:after{ 116 | content: ''; 117 | display: block; 118 | position: absolute; 119 | border: 4px solid transparent; 120 | border-top-color: #000; 121 | right: -7px; 122 | top: 1px; 123 | -webkit-transform: rotate(-45deg); 124 | -moz-transform: rotate(-45deg); 125 | -ms-transform: rotate(-45deg); 126 | -o-transform: rotate(-45deg); 127 | transform: rotate(-45deg); 128 | } 129 | 130 | 131 | .replayTable > .controls-container > .controls > .previous , 132 | .replayTable > .controls-container > .controls > .next{ 133 | width: 14px; 134 | height: 14px; 135 | } 136 | 137 | .replayTable > .controls-container > .controls > .previous:before , 138 | .replayTable > .controls-container > .controls > .next:before { 139 | content: ''; 140 | position: absolute; 141 | display: block; 142 | top: 2px; 143 | width: 8px; 144 | height: 8px; 145 | border-right: 2px solid #000; 146 | border-top: 2px solid #000; 147 | } 148 | .replayTable > .controls-container > .controls > .previous:before { 149 | left: 5px; 150 | 151 | -webkit-transform: rotate(-135deg); 152 | -moz-transform: rotate(-135deg); 153 | -ms-transform: rotate(-135deg); 154 | -o-transform: rotate(-135deg); 155 | transform: rotate(-135deg); 156 | } 157 | .replayTable > .controls-container > .controls > .next:before { 158 | right: 5px; 159 | -webkit-transform: rotate(45deg); 160 | -moz-transform: rotate(45deg); 161 | -ms-transform: rotate(45deg); 162 | -o-transform: rotate(45deg); 163 | transform: rotate(45deg); 164 | } 165 | 166 | 167 | /* SLIDER ---------------------------------------------------------------------- 168 | ----------------------------------------------------------------------------------------- */ 169 | 170 | .replayTable .slider{ 171 | display: inline-block; 172 | margin-left: 20px; 173 | width: 230px; 174 | position: relative; 175 | height: 1px; 176 | background: rgba(0,0,0,0.1); 177 | } 178 | .replayTable .slider .slider-toggle{ 179 | position: absolute; 180 | display: block; 181 | width: auto; 182 | white-space: nowrap; 183 | padding: 0 2px; 184 | 185 | min-width: 12px; 186 | height: 16px; 187 | background: #fff; 188 | border: 1px solid #999; 189 | top: -24px; 190 | cursor: pointer; 191 | border-radius: 3px; 192 | text-align: center; 193 | line-height: 17px; 194 | font-size: 10px; 195 | color: #000; 196 | -webkit-user-select: none; 197 | -moz-user-select: none; 198 | -ms-user-select: none; 199 | user-select: none; 200 | } 201 | .replayTable .slider .slider-toggle:before{ 202 | content: ''; 203 | position: absolute; 204 | bottom: -5px; 205 | left: 50%; 206 | margin-left: -4px; 207 | border-width: 6px 4px 0; 208 | border-style: solid; 209 | border-color: #fff transparent; 210 | display: block; 211 | width: 0; 212 | z-index: 2; 213 | } 214 | .replayTable .slider .slider-toggle:after{ 215 | content: ''; 216 | position: absolute; 217 | bottom: -7px; 218 | left: 50%; 219 | margin-left: -4px; 220 | border-width: 6px 4px 0; 221 | border-style: solid; 222 | border-color: #999 transparent; 223 | display: block; 224 | width: 0; 225 | 226 | } 227 | .replayTable .slider .slider-available{ 228 | position: absolute; 229 | display: block; 230 | height: 100%; 231 | left: 0; 232 | top: 0; 233 | background: rgba(0,0,0,0.1); 234 | } 235 | .replayTable .slider .slider-progress{ 236 | position: absolute; 237 | display: block; 238 | height: 100%; 239 | left: 0; 240 | top: 0; 241 | background: rgba(0,0,0,0.3); 242 | } 243 | 244 | 245 | 246 | 247 | /* drilldown-contorls ---------------------------------------------------------------------- 248 | ----------------------------------------------------------------------------------------- */ 249 | 250 | .drilldown-contorls{} 251 | .drilldown-contorls .back{ 252 | display: inline-block; 253 | vertical-align: middle; 254 | position: relative; 255 | width: 14px; 256 | height: 14px; 257 | margin-right: 10px; 258 | font-size: 0; 259 | cursor: pointer; 260 | } 261 | .drilldown-contorls .back:before{ 262 | content: ''; 263 | position: absolute; 264 | display: block; 265 | top: 2px; 266 | left: 5px; 267 | width: 8px; 268 | height: 8px; 269 | border-right: 2px solid #000; 270 | border-top: 2px solid #000; 271 | -webkit-transform: rotate(-135deg); 272 | -moz-transform: rotate(-135deg); 273 | -ms-transform: rotate(-135deg); 274 | -o-transform: rotate(-135deg); 275 | transform: rotate(-135deg); 276 | } 277 | .drilldown-contorls .item{ 278 | display: inline-block; 279 | vertical-align: bottom; 280 | font-size: 14px; 281 | text-transform: uppercase; 282 | font-weight: 800; 283 | } 284 | 285 | 286 | 287 | 288 | 289 | /* CONTAINER ---------------------------------------------------------------------- 290 | ----------------------------------------------------------------------------------------- */ 291 | 292 | .replayTable .table-container{ 293 | position: relative; 294 | } 295 | .replayTable .table-container table { 296 | position: relative; 297 | left: 0; 298 | top: 0; 299 | border-spacing: 0; 300 | } 301 | .replayTable .table-container table + table{ 302 | position: absolute; 303 | left: 0; 304 | top: 0; 305 | } 306 | 307 | .replayTable .table-container table.drilldown{ 308 | position: relative; 309 | } 310 | .replayTable .hidden { 311 | visibility: hidden; 312 | 313 | } 314 | .replayTable .table-container.classic.drilldowned .hidden{ 315 | display: none; 316 | } 317 | .replayTable th{ 318 | text-align: left; 319 | text-transform: uppercase; 320 | font-size: 12px; 321 | padding-bottom: 5px; 322 | border-bottom: 1px dotted rgba(0,0,0,0.1); 323 | } 324 | .replayTable td{ 325 | white-space: nowrap; 326 | padding: 0 5px; 327 | font-size: 14px; 328 | line-height: 14px; 329 | min-height: 23px; 330 | height: 23px; 331 | 332 | } 333 | .replayTable td.outcome{ 334 | width: 5px; 335 | padding: 0; 336 | min-height: 23px; 337 | } 338 | .replayTable .clickable{ 339 | cursor: pointer; 340 | } 341 | .replayTable .clickable:hover{ 342 | text-decoration: underline; 343 | } 344 | .replayTable .calculation{ 345 | text-align: center; 346 | } 347 | 348 | 349 | 350 | 351 | 352 | 353 | .replayTable .controls-container.sparklines{ 354 | margin-bottom: 10px; 355 | } 356 | .replayTable .table-container.sparklines{} 357 | .replayTable .table-container.sparklines:after{ 358 | content: ''; 359 | display: block; 360 | clear: both; 361 | } 362 | .replayTable .table-container.sparklines > table{ 363 | float: left; 364 | position: relative; 365 | } 366 | /*.replayTable .table-container.sparklines > table.sparks{*/ 367 | /*top: -7px;*/ 368 | /*}*/ 369 | .replayTable .table-container.sparklines > table td{ 370 | border-bottom: 2px solid #fff; 371 | } 372 | .replayTable .table-container.sparklines .slider-cell{ 373 | height: 0; 374 | } 375 | .replayTable td.spark { 376 | width: 5px !important; 377 | max-width: 5px; 378 | height: 17px; 379 | padding: 3px 0; 380 | position: relative; 381 | white-space: nowrap; 382 | font-size: 14px; 383 | min-height: 23px; 384 | } 385 | 386 | .replayTable td.spark .spark-position{ 387 | position: absolute; 388 | display: block; 389 | width: 100%; 390 | height: 1px !important; 391 | line-height: 1px; 392 | font-size: 1px; 393 | background: #000; 394 | left: 0; 395 | } 396 | .replayTable td.spark.muted .spark-position{ 397 | opacity: 0.4; 398 | } 399 | 400 | .replayTable td.change { 401 | color: rgba(0,0,0,0.8); 402 | font-size: 14px; 403 | } 404 | .replayTable .main.right td.change{ 405 | text-align: center; 406 | } 407 | .replayTable .main.right td.opponent.change, 408 | .replayTable .main.right td.label.change{ 409 | text-align: left; 410 | } 411 | 412 | .replayTable td.spark.muted { 413 | background-color: transparent!important; 414 | } 415 | 416 | .replayTable td.spark.overlapped { 417 | -webkit-filter: grayscale(100%); 418 | -moz-filter: grayscale(100%); 419 | -ms-filter: grayscale(100%); 420 | -o-filter: grayscale(100%); 421 | filter: grayscale(100%); 422 | filter: gray; /* IE 6-9 */ 423 | } 424 | 425 | 426 | .replayTable span.spark-score{ 427 | position: absolute; 428 | left: 7px; 429 | top: 0; 430 | line-height: 23px; 431 | } 432 | .replayTable td.spark.muted{} 433 | .replayTable span.spark-score.muted { 434 | visibility: hidden; 435 | } 436 | 437 | .replayTable .sparklines tr.muted { 438 | visibility: hidden; 439 | } 440 | 441 | .replayTable .sparklines tr.muted > td.opponent { 442 | font-size: 0; 443 | } 444 | 445 | .replayTable .main.right{ 446 | z-index: 2; 447 | background: rgba(255,255,255,0.3); 448 | } 449 | 450 | .controls-container.sparklines .drilldown-control{ 451 | display: inline-block; 452 | vertical-align: bottom; 453 | margin-left: 10px; 454 | line-height: 13px; 455 | font-size: 14px; 456 | text-transform: uppercase; 457 | font-weight: 800; 458 | } 459 | 460 | 461 | 462 | 463 | .replayTable .sparklines-slider .slider-cell{ 464 | position: relative; 465 | } 466 | .replayTable .sparklines-slider.top .slider-toggle{ 467 | position: absolute; 468 | display: block; 469 | width: auto; 470 | white-space: nowrap; 471 | padding: 0 2px; 472 | 473 | min-width: 12px; 474 | height: 16px; 475 | background: #fff; 476 | border: 1px solid #999; 477 | top: -23px; 478 | margin-left: -6px; 479 | cursor: pointer; 480 | border-radius: 3px; 481 | text-align: center; 482 | line-height: 17px; 483 | font-size: 10px; 484 | color: #000; 485 | -webkit-user-select: none; 486 | -moz-user-select: none; 487 | -ms-user-select: none; 488 | user-select: none; 489 | } 490 | .replayTable .sparklines-slider.top .slider-toggle:before{ 491 | content: ''; 492 | position: absolute; 493 | bottom: -5px; 494 | left: 50%; 495 | margin-left: -4px; 496 | border-width: 6px 4px 0; 497 | border-style: solid; 498 | border-color: #fff transparent; 499 | display: block; 500 | width: 0; 501 | z-index: 2; 502 | } 503 | .replayTable .sparklines-slider.top .slider-toggle:after{ 504 | content: ''; 505 | position: absolute; 506 | bottom: -7px; 507 | left: 50%; 508 | margin-left: -4px; 509 | border-width: 6px 4px 0; 510 | border-style: solid; 511 | border-color: #999 transparent; 512 | display: block; 513 | width: 0; 514 | 515 | } 516 | 517 | 518 | .replayTable .sparklines-slider.bottom .slider-toggle{ 519 | position: absolute; 520 | display: block; 521 | width: auto; 522 | white-space: nowrap; 523 | padding: 0 2px; 524 | 525 | min-width: 12px; 526 | height: 16px; 527 | background: #fff; 528 | border: 1px solid #999; 529 | bottom: -23px; 530 | margin-left: -6px; 531 | cursor: pointer; 532 | border-radius: 3px; 533 | text-align: center; 534 | line-height: 17px; 535 | font-size: 10px; 536 | color: #000; 537 | -webkit-user-select: none; 538 | -moz-user-select: none; 539 | -ms-user-select: none; 540 | user-select: none; 541 | } 542 | .replayTable .sparklines-slider.bottom .slider-toggle:before{ 543 | content: ''; 544 | position: absolute; 545 | top: -6px; 546 | left: 50%; 547 | margin-left: -4px; 548 | border-width: 0 4px 6px; 549 | border-style: solid; 550 | border-color: #fff transparent; 551 | display: block; 552 | width: 0; 553 | z-index: 2; 554 | } 555 | .replayTable .sparklines-slider.bottom .slider-toggle:after{ 556 | content: ''; 557 | position: absolute; 558 | top: -7px; 559 | left: 50%; 560 | margin-left: -4px; 561 | border-width: 0 4px 6px; 562 | border-style: solid; 563 | border-color: #999 transparent; 564 | display: block; 565 | width: 0; 566 | } -------------------------------------------------------------------------------- /src/replay-table.js: -------------------------------------------------------------------------------- 1 | import './replay-table.css'; 2 | 3 | export {default as configure } from './configure/configure'; 4 | export {default as extract } from './extract/extract'; 5 | export {default as transform } from './transform/transform'; 6 | export {default as calculate } from './calculate/calculate'; 7 | export {default as visualize } from './visualize/visualize'; 8 | export {default as magic } from './magic'; 9 | -------------------------------------------------------------------------------- /src/transform/config.js: -------------------------------------------------------------------------------- 1 | import * as transformers from '../transform/transformers'; 2 | import validateObject from '../helpers/validation/validate-object'; 3 | import validateArray from '../helpers/validation/validate-array'; 4 | import isString from '../helpers/general/is-string'; 5 | import parseObject from '../helpers/parsing/parse-object'; 6 | 7 | 8 | export default { 9 | id: { 10 | default: '', 11 | parse: input => input, 12 | validate: isString 13 | }, 14 | 15 | transformer: { 16 | default: 'listOfMatches', 17 | parse: value => value, 18 | validate: value => transformers.hasOwnProperty(value) 19 | }, 20 | 21 | changeToOutcome: { 22 | default: { 23 | 3: 'win', 24 | 1: 'draw', 25 | 0: 'loss' 26 | }, 27 | parse: input => parseObject(input), 28 | validate: obj => validateObject(obj, 29 | key => !Number.isNaN(key), 30 | value => ['win', 'draw', 'loss'].includes(value)) 31 | }, 32 | 33 | //post-transformers 34 | filterItems: { 35 | default: [], 36 | parse: input => input.split(','), 37 | validate: value => validateArray(value, isString) 38 | }, 39 | 40 | insertStartRound: { 41 | default: '0', 42 | parse: input => input, 43 | validate: isString 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/transform/configs/index.js: -------------------------------------------------------------------------------- 1 | export {default as pointsTable} from './points-table'; 2 | export {default as listOfMatches} from './list-of-matches'; 3 | -------------------------------------------------------------------------------- /src/transform/configs/list-of-matches.js: -------------------------------------------------------------------------------- 1 | export default { 2 | format: { 3 | default: 'csv', 4 | parse: input => input, 5 | validate: value => ['csv', 'football-data.org'].includes(value) 6 | }, 7 | 8 | locationFirst: { 9 | default: 'home', 10 | parse: input => input, 11 | validate: value => ['home', 'away'].includes(value) 12 | }, 13 | 14 | collapseToRounds: { 15 | default: false, 16 | parse: input => input === 'true', 17 | validate: value => typeof value === 'boolean' 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/transform/configs/points-table.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extraColumnsNumber: { 3 | default: 0, 4 | parse: input => Number.parseInt(input, 10), 5 | validate: value => !Number.isNaN(value) 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/transform/helpers/match/flip.js: -------------------------------------------------------------------------------- 1 | export default function (result) { 2 | return { 3 | team: result.match.opponent, 4 | match: { 5 | location: flipLocation(result.match.location), 6 | score: result.match.opponentScore, 7 | opponent: result.team, 8 | opponentScore: result.match.score 9 | } 10 | }; 11 | } 12 | 13 | function flipLocation (location) { 14 | switch (location) { 15 | case 'home': 16 | return 'away'; 17 | case 'away': 18 | return 'home'; 19 | case 'neutral': 20 | return 'neutral'; 21 | default: 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/transform/helpers/match/getOutcome.js: -------------------------------------------------------------------------------- 1 | export default function (result) { 2 | if (result.match.score === null || result.match.opponentScore === null) { 3 | return null; 4 | } 5 | 6 | if (result.match.score > result.match.opponentScore) { 7 | return 'win'; 8 | } else if (result.match.score < result.match.opponentScore) { 9 | return 'loss'; 10 | } else if (result.match.score === result.match.opponentScore) { 11 | return 'draw'; 12 | } else { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/transform/post-transformers/collapse-to-rounds.js: -------------------------------------------------------------------------------- 1 | import getItems from '../../helpers/data/get-items'; 2 | 3 | 4 | export default function (transformedData) { 5 | const items = getItems(transformedData); 6 | const itemRound = items.reduce((obj, item) => Object.assign(obj, { [item]: 0 }), {}); 7 | 8 | const collapsed = []; 9 | 10 | transformedData.forEach(round => { 11 | round.results 12 | .filter(result => result.change !== null) 13 | .forEach(result => { 14 | const roundNumber = ++itemRound[result.item]; 15 | 16 | if (collapsed.length < roundNumber) { 17 | collapsed.push({ 18 | name: roundNumber.toString(), 19 | results: [] 20 | }); 21 | } 22 | 23 | collapsed[roundNumber - 1].results.push(result); 24 | }); 25 | }); 26 | 27 | return collapsed; 28 | }; 29 | -------------------------------------------------------------------------------- /src/transform/post-transformers/filter-items.js: -------------------------------------------------------------------------------- 1 | export default function (transformedData, items) { 2 | return transformedData.map(round => { 3 | return { 4 | name: round.name, 5 | results: round.results.filter(result => items.includes(result.item)) 6 | } 7 | }) 8 | }; 9 | -------------------------------------------------------------------------------- /src/transform/post-transformers/index.js: -------------------------------------------------------------------------------- 1 | export {default as collapseToRounds} from './collapse-to-rounds'; 2 | export {default as filterItems} from './filter-items'; 3 | export {default as insertStartRound} from './insert-start-round'; 4 | -------------------------------------------------------------------------------- /src/transform/post-transformers/insert-start-round.js: -------------------------------------------------------------------------------- 1 | export default function (transformedData, roundName) { 2 | const startRoundResults = transformedData[0].results.map(result => { 3 | const startResult = Object.assign({}, result); 4 | 5 | ['change', 'match', 'outcome'] 6 | .filter(key => result.hasOwnProperty(key)) 7 | .forEach(key => startResult[key] = null); 8 | 9 | if (startResult.extras) { 10 | Object.keys(startResult.extras) 11 | .filter(key => key !== 'item') 12 | .forEach(key => startResult.extras[key] = null); 13 | } 14 | 15 | return startResult; 16 | }); 17 | 18 | return [{ 19 | name: roundName, 20 | results: startRoundResults 21 | }].concat(transformedData); 22 | }; 23 | -------------------------------------------------------------------------------- /src/transform/transform.js: -------------------------------------------------------------------------------- 1 | import moduleConfig from './config'; 2 | import * as transformersConfig from './configs'; 3 | import initialize from '../configure/initialize' 4 | import * as transformers from './transformers'; 5 | import * as postTransformers from './post-transformers'; 6 | 7 | 8 | export default function (rawData, userConfig) { 9 | const params = initialize('transformer', moduleConfig, transformersConfig, userConfig); 10 | 11 | const transformed = transformers[params.transformer](rawData, params); 12 | 13 | const filtered = params.filterItems.length > 0 14 | ? postTransformers.filterItems(transformed, params.filterItems) 15 | : transformed; 16 | 17 | const collapsed = params.collapseToRounds 18 | ? postTransformers.collapseToRounds(filtered) 19 | : filtered; 20 | 21 | return params.insertStartRound 22 | ? postTransformers.insertStartRound(collapsed, params.insertStartRound) 23 | : collapsed; 24 | }; 25 | -------------------------------------------------------------------------------- /src/transform/transformers/index.js: -------------------------------------------------------------------------------- 1 | export {default as pointsTable} from './points-table'; 2 | export {default as listOfMatches} from './list-of-matches'; 3 | -------------------------------------------------------------------------------- /src/transform/transformers/list-of-matches.js: -------------------------------------------------------------------------------- 1 | import flipObject from '../../helpers/general/flip-object'; 2 | import flipMatchResult from '../helpers/match/flip'; 3 | import getMatchOutcome from '../helpers/match/getOutcome'; 4 | 5 | 6 | export default function (rawData, params) { 7 | const list = new List(rawData, params.format); 8 | const outcomeToChange = flipObject(params.changeToOutcome); 9 | 10 | return list.roundsNames.map(roundName => { 11 | const roundResults = []; 12 | list.matches.filter(match => list.getRoundName(match) === roundName) 13 | .forEach(match => { 14 | const firstTeamResult = { 15 | team: list.getFirstTeam(match), 16 | match: { 17 | location: params.locationFirst, 18 | score: list.getScore(match), 19 | opponent: list.getSecondTeam(match), 20 | opponentScore: list.getOpponentScore(match) 21 | } 22 | }; 23 | 24 | [firstTeamResult, flipMatchResult(firstTeamResult)].forEach(teamResult => { 25 | const outcome = getMatchOutcome(teamResult); 26 | 27 | roundResults.push({ 28 | item: teamResult.team, 29 | change: outcome ? outcomeToChange[outcome] : null, 30 | outcome: outcome, 31 | match: teamResult.match, 32 | extras: {} 33 | }); 34 | }); 35 | }); 36 | 37 | 38 | list.itemsNames.filter(name => !roundResults.map(result => result.item).includes(name)) 39 | .forEach(name => { 40 | roundResults.push({ 41 | item: name, 42 | change: null, 43 | match: null, 44 | extras: {} 45 | }); 46 | }); 47 | 48 | return { 49 | name: roundName, 50 | results: roundResults 51 | }; 52 | }); 53 | } 54 | 55 | class List { 56 | constructor(data, format) { 57 | this.data = data; 58 | this.format = format; 59 | 60 | switch (format) { 61 | case 'csv': 62 | const [headers, ...matches] = data.filter(row => row && row.length >= 5); 63 | this.matches = matches; 64 | break; 65 | case 'football-data.org': 66 | this.matches = data.fixtures; 67 | break; 68 | } 69 | 70 | this.roundsNames = [...new Set(this.matches.map(match => this.getRoundName(match)))]; 71 | this.itemsNames = [...new Set([...this.matches.map(match => this.getFirstTeam(match)), 72 | ...this.matches.map(match => this.getSecondTeam(match))])]; 73 | } 74 | 75 | getRoundName (match) { 76 | switch (this.format) { 77 | case 'csv': 78 | return match[0]; 79 | case 'football-data.org': 80 | return match.matchday.toString(); 81 | } 82 | } 83 | 84 | getFirstTeam (match) { 85 | switch (this.format) { 86 | case 'csv': 87 | return match[1]; 88 | case 'football-data.org': 89 | return match.homeTeamName; 90 | } 91 | } 92 | 93 | getSecondTeam (match) { 94 | switch (this.format) { 95 | case 'csv': 96 | return match[3]; 97 | case 'football-data.org': 98 | return match.awayTeamName; 99 | } 100 | } 101 | 102 | getScore (match) { 103 | switch (this.format) { 104 | case 'csv': 105 | return Number.parseInt(match[2], 10); 106 | case 'football-data.org': 107 | return match.status === 'FINISHED' ? match.result.goalsHomeTeam : null; 108 | } 109 | } 110 | 111 | getOpponentScore (match) { 112 | switch (this.format) { 113 | case 'csv': 114 | return Number.parseInt(match[4], 10) 115 | case 'football-data.org': 116 | return match.status === 'FINISHED' ? match.result.goalsAwayTeam : null; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/transform/transformers/points-table.js: -------------------------------------------------------------------------------- 1 | import transpose from '../../helpers/general/transpose'; 2 | 3 | 4 | export default function (rawData, params) { 5 | const offset = (params.extraColumnsNumber || 0) + 1; 6 | 7 | const extraColumnsNames = rawData[0].slice(1, offset); 8 | const roundsNames = rawData[0].slice(offset); 9 | 10 | const transposed = transpose(rawData.slice(1).filter(row => row[0])); 11 | const itemsNames = transposed[0]; 12 | const extraColumns = transposed.slice(1, offset) 13 | .map(column => new Map(itemsNames.map((item, i) => [item, column[i]]))); 14 | const changes = transposed.slice(offset); 15 | 16 | 17 | return changes.map((resultRow, rowIndex) => { 18 | const roundResults = []; 19 | resultRow.forEach((changeString, itemNumber) => { 20 | const item = itemsNames[itemNumber]; 21 | const change = changeString ? Number.parseInt(changeString, 10) || 0 : null; 22 | 23 | roundResults.push({ 24 | item: item, 25 | change: change, 26 | outcome: params.changeToOutcome[change] || null, 27 | extras: { 28 | item: extraColumns.reduce((obj, col, i) => Object.assign(obj, { [extraColumnsNames[i]]: col.get(item) }), {}) 29 | } 30 | }); 31 | }); 32 | 33 | return { 34 | name: roundsNames[rowIndex], 35 | results: roundResults 36 | }; 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/visualize/cell.js: -------------------------------------------------------------------------------- 1 | import calculations from '../calculate/calculations'; 2 | import numberToChange from '../helpers/general/number-to-change'; 3 | import formatPosition from './helpers/format-position'; 4 | import mapParamToModule from '../configure/helpers/map-param-to-module'; 5 | 6 | 7 | export default class Cell { 8 | constructor (column, result, params) { 9 | this.column = column; 10 | this.result = result; 11 | 12 | if (this[column]) { 13 | return this[column](result, params); 14 | } else if (calculations.hasOwnProperty(column)) { 15 | return this.makeCalculation(column, result, params); 16 | } else if (column.includes('.change')) { 17 | return this.makeChange(column, result, params); 18 | } else if (column.includes('spark')) { 19 | return this.makeSpark(column, result, params); 20 | } else if (mapParamToModule(column, result.extras)) { 21 | return this.makeExtra(column, result, params); 22 | } else { 23 | this.text = ''; 24 | this.classes = []; 25 | return this; 26 | } 27 | } 28 | 29 | position (result, params) { 30 | this.text = formatPosition(result.position, params.positionWhenTied); 31 | this.classes = ['position']; 32 | return this; 33 | } 34 | 35 | item (result, params) { 36 | this.text = result.item; 37 | this.classes = ['item', 'clickable']; 38 | return this; 39 | } 40 | 41 | match (result, params) { 42 | this.text = result.match ? `${result.match.score}-${result.match.opponentScore} ${result.match.opponent}` : ''; 43 | this.classes = ['change']; 44 | return this; 45 | } 46 | 47 | outcome (result, params) { 48 | this.text = ''; 49 | this.classes = ['outcome']; 50 | this.backgroundColor = params.colors[result.outcome] || 'transparent'; 51 | return this; 52 | } 53 | 54 | goalsDifference (result, params) { 55 | this.text = numberToChange(result.goalsDifference.total, '0'); 56 | this.classes = ['calculation']; 57 | return this; 58 | } 59 | 60 | winningPercentage (result, params) { 61 | this.text = result.winningPercentage.total.toFixed(3).toString().replace('0',''); 62 | this.classes = ['calculation']; 63 | return this; 64 | } 65 | 66 | round (result, params) { 67 | this.text = result.roundMeta.name; 68 | this.classes = ['round', 'clickable']; 69 | return this; 70 | } 71 | 72 | makeCalculation (column, result, params) { 73 | this.text = result[column].total; 74 | this.classes = ['calculation']; 75 | return this; 76 | } 77 | 78 | makeChange (column, result, params) { 79 | const calc = column.replace('.change', ''); 80 | this.text = numberToChange(result[calc].change); 81 | this.classes = ['change']; 82 | return this; 83 | } 84 | 85 | makeSpark (column, result, params) { 86 | this.text = ''; 87 | this.classes = ['spark']; 88 | 89 | this.roundIndex = Number.parseInt(column.split('.')[1]); 90 | const itemResults = params.sparklinesData.get(result.item); 91 | 92 | if (this.roundIndex >= itemResults.length) { 93 | this.backgroundColor = 'transparent'; 94 | this.result = {}; 95 | } else { 96 | this.result = itemResults[this.roundIndex]; 97 | 98 | if (this.roundIndex === params.currentRound) { 99 | this.classes.push('current'); 100 | this.backgroundColor = params.currentSparkColors[itemResults[this.roundIndex].outcome] || 'transparent'; 101 | } else { 102 | this.backgroundColor = params.sparkColors[itemResults[this.roundIndex].outcome] || 'transparent'; 103 | } 104 | } 105 | 106 | return this; 107 | } 108 | 109 | makeExtra (column, result, params) { 110 | const extraType = mapParamToModule(column, result.extras); 111 | this.text = result.extras[extraType][column]; 112 | this.classes = [`extra-${extraType}`]; 113 | return this; 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /src/visualize/config.js: -------------------------------------------------------------------------------- 1 | import * as visualizers from './visualizers'; 2 | import * as controls from './controls'; 3 | import isString from '../helpers/general/is-string'; 4 | import parseObject from '../helpers/parsing/parse-object'; 5 | import validateObject from '../helpers/validation/validate-object'; 6 | import validateArray from '../helpers/validation/validate-array'; 7 | 8 | 9 | export default { 10 | id: { 11 | default: '', 12 | parse: input => input, 13 | validate: isString 14 | }, 15 | 16 | visualizer: { 17 | default: 'classic', 18 | parse: input => input, 19 | validate: value => visualizers.hasOwnProperty(value) 20 | }, 21 | 22 | controls: { 23 | default: ['play', 'previous', 'next', 'slider'], 24 | parse: input => input.split(','), 25 | validate: value => validateArray(value, value => controls.hasOwnProperty(value)) 26 | }, 27 | 28 | startFromRound: { 29 | default: null, 30 | parse: input => Number.parseInt(input, 10), 31 | validate: value => !value || !Number.isNaN(value) 32 | }, 33 | 34 | roundsTotalNumber: { 35 | default: null, 36 | parse: input => Number.parseInt(input, 10) || undefined, 37 | validate: value => !value || !Number.isNaN(value) 38 | }, 39 | 40 | positionWhenTied: { 41 | default: 'strict', 42 | parse: input => input, 43 | validate: value => ['strict', 'highest', 'range', 'average'].includes(value) 44 | }, 45 | 46 | animationSpeed: { 47 | default: 1.0, 48 | parse: Number.parseFloat, 49 | validate: value => !Number.isNaN(value) && value > 0.0 && value <= 10.0 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/visualize/configs/classic.js: -------------------------------------------------------------------------------- 1 | import validateArray from '../../helpers/validation/validate-array'; 2 | import isString from '../../helpers/general/is-string'; 3 | import parseObject from '../../helpers/parsing/parse-object'; 4 | import validateObject from '../../helpers/validation/validate-object'; 5 | 6 | export default { 7 | columns: { 8 | default: ['position', 'item', 'points'], 9 | parse: input => input.split(','), 10 | validate: value => validateArray(value, isString) 11 | }, 12 | 13 | labels: { 14 | default: ['#', 'Team', 'Points'], 15 | parse: input => input.split(','), 16 | validate: value => validateArray(value, isString) 17 | }, 18 | 19 | colors: { 20 | default: { 21 | 'win': '#ACE680', 22 | 'draw': '#B3B3B3', 23 | 'loss': '#E68080' 24 | }, 25 | parse: parseObject, 26 | validate: obj => validateObject(obj, 27 | key => ['win', 'draw', 'loss'].includes(key), 28 | value => isString(value)) 29 | }, 30 | 31 | durations: { 32 | default: { 33 | move: 750, 34 | freeze: 750, 35 | outcomes: 200 36 | }, 37 | parse: parseObject, 38 | validate: obj => validateObject(obj, 39 | key => ['move', 'freeze', 'outcomes'].includes(key), 40 | value => !Number.isNaN(value) && value >= 0) 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/visualize/configs/index.js: -------------------------------------------------------------------------------- 1 | export {default as classic} from './classic'; 2 | export {default as sparklines} from './sparklines'; 3 | -------------------------------------------------------------------------------- /src/visualize/configs/sparklines.js: -------------------------------------------------------------------------------- 1 | import validateArray from '../../helpers/validation/validate-array'; 2 | import isString from '../../helpers/general/is-string'; 3 | import * as controls from '../controls'; 4 | import parseObject from '../../helpers/parsing/parse-object'; 5 | import validateObject from '../../helpers/validation/validate-object'; 6 | 7 | export default { 8 | controls: { 9 | default: ['play'], 10 | parse: input => input.split(','), 11 | validate: value => validateArray(value, value => controls.hasOwnProperty(value)) 12 | }, 13 | 14 | colors: { 15 | default: { 16 | 'win': '#21c114', 17 | 'draw': '#828282', 18 | 'loss': '#e63131' 19 | }, 20 | parse: parseObject, 21 | validate: obj => validateObject(obj, 22 | key => ['win', 'draw', 'loss'].includes(key), 23 | value => isString(value)) 24 | }, 25 | 26 | sparkColors: { 27 | default: { 28 | 'win': '#D7E7C1', 29 | 'draw': '#F0F0F0', 30 | 'loss': '#EFCEBA' 31 | }, 32 | parse: parseObject, 33 | validate: obj => validateObject(obj, 34 | key => ['win', 'draw', 'loss'].includes(key), 35 | value => isString(value)) 36 | }, 37 | 38 | currentSparkColors: { 39 | default: { 40 | 'win': '#AAD579', 41 | 'draw': '#CCCCCC', 42 | 'loss': '#E89B77' 43 | }, 44 | parse: parseObject, 45 | validate: obj => validateObject(obj, 46 | key => ['win', 'draw', 'loss'].includes(key), 47 | value => isString(value)) 48 | }, 49 | 50 | durations: { 51 | default: { 52 | move: 1000, 53 | freeze: 500, 54 | pre: 750 55 | }, 56 | parse: parseObject, 57 | validate: obj => validateObject(obj, 58 | key => ['move', 'freeze', 'pre'].includes(key), 59 | value => !Number.isNaN(value) && value >= 0) 60 | }, 61 | 62 | pointsLabel: { 63 | default: 'points', 64 | parse: input => input, 65 | validate: isString 66 | }, 67 | 68 | allLabel: { 69 | default: 'All', 70 | parse: input => input, 71 | validate: isString 72 | }, 73 | 74 | shortOutcomeLabels: { 75 | default: { 76 | 'win': 'w.', 77 | 'draw': 'd.', 78 | 'loss': 'l.', 79 | }, 80 | parse: parseObject, 81 | validate: obj => validateObject(obj, 82 | key => ['win', 'draw', 'loss'].includes(key), 83 | value => isString(value)) 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /src/visualize/controls/index.js: -------------------------------------------------------------------------------- 1 | export {default as play} from './play'; 2 | export {default as previous} from './previous'; 3 | export {default as next} from './next'; 4 | export {default as slider} from './slider'; 5 | -------------------------------------------------------------------------------- /src/visualize/controls/next.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor (selector, roundMeta, next) { 3 | this.button = selector.append('div') 4 | .attr('class', 'next') 5 | .classed('disabled', roundMeta.isLast) 6 | .on('click', next); 7 | 8 | this.onRoundChange = this.onRoundChange.bind(this); 9 | } 10 | 11 | onRoundChange (roundMeta) { 12 | this.button.classed('disabled', roundMeta.isLast); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/visualize/controls/play.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor (selector, roundMeta, play, pause) { 3 | this.isLast = roundMeta.isLast; 4 | 5 | this.button = selector.append('div') 6 | .on('click', () => { 7 | if (this.isPlaying) { 8 | pause(); 9 | } else { 10 | play(); 11 | } 12 | }); 13 | 14 | this.updateClass(); 15 | 16 | this.onPlay = this.onPlay.bind(this); 17 | this.onPause = this.onPause.bind(this); 18 | this.onRoundChange = this.onRoundChange.bind(this); 19 | } 20 | 21 | onPlay () { 22 | this.isPlaying = true; 23 | this.updateClass(); 24 | } 25 | 26 | onPause () { 27 | this.isPlaying = false; 28 | this.updateClass(); 29 | } 30 | 31 | onRoundChange (roundMeta) { 32 | this.isLast = roundMeta.isLast; 33 | this.updateClass(); 34 | } 35 | 36 | updateClass () { 37 | const className = this.isPlaying 38 | ? 'pause' 39 | : this.isLast ? 'replay' : 'play'; 40 | 41 | this.button 42 | .classed('play', className === 'play') 43 | .classed('pause', className === 'pause') 44 | .classed('replay', className === 'replay'); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/visualize/controls/previous.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor (selector, roundMeta, previous) { 3 | this.button = selector.append('div') 4 | .attr('class', 'previous') 5 | .classed('disabled', roundMeta.index === 0) 6 | .on('click', previous); 7 | 8 | this.onRoundChange = this.onRoundChange.bind(this); 9 | } 10 | 11 | onRoundChange (roundMeta) { 12 | this.button.classed('disabled', roundMeta.index === 0); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/visualize/controls/slider.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor (selector, roundsAvailable, roundsTotal, roundMeta, preview, endPreview) { 3 | this.container = selector.append('div') 4 | .attr('class', 'slider'); 5 | 6 | this.roundToPercent = d3.scaleLinear() 7 | .domain([0, roundsTotal]) 8 | .range([0, 100]); 9 | 10 | const rectangle = this.container.node().getBoundingClientRect(); 11 | this.scale = d3.scaleLinear() 12 | .range([0, rectangle.right - rectangle.left]) 13 | .domain([0, roundsTotal]) 14 | .clamp(true); 15 | 16 | this.available = this.container.append('span') 17 | .attr('class', 'slider-available') 18 | .style('width', `${this.roundToPercent(roundsAvailable)}%`); 19 | 20 | const progress = `${this.roundToPercent(roundMeta.index)}%`; 21 | 22 | this.toggle = this.container.append('span') 23 | .attr('class', 'slider-toggle') 24 | .style('left', progress) 25 | .text(roundMeta.name) 26 | .style('margin-left', this.adaptMargin) 27 | .call(d3.drag() 28 | .on("drag", () => { 29 | const round = Math.min(Math.round(this.scale.invert(d3.event.x)), roundsAvailable); 30 | preview(round); 31 | }) 32 | .on("end", () => endPreview(true))); 33 | 34 | this.progress = this.container.append('span') 35 | .attr('class', 'slider-progress') 36 | .style('width', progress); 37 | 38 | this.onRoundPreview = this.onRoundPreview.bind(this); 39 | this.onRoundChange = this.onRoundChange.bind(this); 40 | } 41 | 42 | adaptMargin () { 43 | const width = d3.select(this).node().getBoundingClientRect().width; 44 | return `-${width/2}px`; 45 | } 46 | 47 | onRoundPreview (roundMeta) { 48 | const progress = `${this.roundToPercent(roundMeta.index)}%`; 49 | 50 | this.toggle 51 | .style('left', progress) 52 | .text(roundMeta.name) 53 | .style('margin-left', this.adaptMargin); 54 | 55 | this.progress 56 | .style('width', progress); 57 | } 58 | 59 | onRoundChange (roundMeta) { 60 | const progress = `${this.roundToPercent(roundMeta.index)}%`; 61 | 62 | this.toggle 63 | .transition() 64 | .duration(500) 65 | .styleTween('left', () => d3.interpolateString(this.toggle.node().style.left, progress)) 66 | .on('end', () => { 67 | this.toggle.text(roundMeta.name) 68 | .style('margin-left', this.adaptMargin); 69 | }); 70 | 71 | this.progress 72 | .transition() 73 | .duration(500) 74 | .styleTween('width', () => d3.interpolateString(this.progress.node().style.width, progress)); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/visualize/helpers/adjust-durations.js: -------------------------------------------------------------------------------- 1 | export default function (durations, speed) { 2 | return Object.keys(durations) 3 | .reduce((obj, key) => Object.assign(obj, { [key]: durations[key]/speed }), {}) 4 | }; 5 | -------------------------------------------------------------------------------- /src/visualize/helpers/format-position.js: -------------------------------------------------------------------------------- 1 | export default function (position, positionWhenTied) { 2 | switch (positionWhenTied) { 3 | case 'strict': 4 | return position.strict.toString(); 5 | case 'highest': 6 | return position.highest.toString(); 7 | case 'range': 8 | if (position.highest !== position.lowest) { 9 | return `${position.highest}–${position.lowest}`; 10 | } else { 11 | return position.highest.toString(); 12 | } 13 | case 'average': 14 | return ((position.highest + position.lowest) / 2).toString(); 15 | default: 16 | return position.strict.toString(); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/visualize/helpers/get-rows-ys.js: -------------------------------------------------------------------------------- 1 | export default function (rows) { 2 | const ys = {}; 3 | rows.nodes().forEach(n => { 4 | const item = n.__data__.item; 5 | const top = n.getBoundingClientRect().top; 6 | if (!ys[item] || ys[item] < top) { 7 | ys[item] = top 8 | } 9 | }); 10 | 11 | return ys; 12 | }; 13 | -------------------------------------------------------------------------------- /src/visualize/helpers/sparklines/get-spark-classes.js: -------------------------------------------------------------------------------- 1 | export default function (cell, roundIndex) { 2 | const classes = ['spark']; 3 | 4 | if (cell.roundMeta.index === roundIndex) { 5 | classes.push('current'); 6 | } else if (cell.roundMeta.index > roundIndex) { 7 | classes.push('overlapped'); 8 | } 9 | 10 | return classes.join(' '); 11 | }; 12 | -------------------------------------------------------------------------------- /src/visualize/helpers/sparklines/get-spark-color.js: -------------------------------------------------------------------------------- 1 | export default function (cell, roundIndex, params) { 2 | if (!cell.result.outcome) { 3 | return 'transparent' 4 | } 5 | 6 | return cell.roundMeta.index === roundIndex 7 | ? params.currentSparkColors[cell.result.outcome] 8 | : params.sparkColors[cell.result.outcome]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/visualize/skeleton.js: -------------------------------------------------------------------------------- 1 | import * as Controls from './controls'; 2 | import adjustDurations from './helpers/adjust-durations'; 3 | import getRowsYs from './helpers/get-rows-ys'; 4 | import toCamelCase from '../helpers/general/to-camel-case'; 5 | 6 | 7 | const dispatchers = ['roundChange', 'play', 'pause', 'roundPreview', 'endPreview', 'drillDown', 'endDrillDown']; 8 | 9 | 10 | export default class { 11 | constructor (data, params) { 12 | this.data = data; 13 | this.params = params; 14 | 15 | this.play = this.play.bind(this); 16 | this.pause = this.pause.bind(this); 17 | this.previous = this.previous.bind(this); 18 | this.next = this.next.bind(this); 19 | this.preview = this.preview.bind(this); 20 | this.endPreview = this.endPreview.bind(this); 21 | this.drillDown = this.drillDown.bind(this); 22 | this.endDrillDown = this.endDrillDown.bind(this); 23 | 24 | this.durations = adjustDurations(params.durations, params.animationSpeed); 25 | 26 | this.roundsTotalNumber = this.params.roundsTotalNumber || this.data.meta.lastRound; 27 | this.currentRound = params.startFromRound === null ? this.data.meta.lastRound : params.startFromRound; 28 | this.previewedRound = null; 29 | this.drilldown = {}; 30 | 31 | this.dispatch = d3.dispatch(...dispatchers); 32 | this.dispatch.on('roundChange', roundMeta => this.currentRound = roundMeta.index); 33 | this.dispatch.on('play', () => this.isPlaying = true); 34 | this.dispatch.on('pause', () => this.isPlaying = false); 35 | 36 | this.dispatch.on('roundPreview', roundMeta => this.previewedRound = roundMeta.index); 37 | this.dispatch.on('endPreview', roundMeta => this.previewedRound = null); 38 | this.dispatch.on('drillDown', item => { 39 | this.tableContainer.classed('drilldowned', true); 40 | this.drilldown.item = item 41 | }); 42 | this.dispatch.on('endDrillDown', item => { 43 | this.tableContainer.classed('drilldowned', false); 44 | this.drilldown = {} 45 | }); 46 | 47 | this.selector = params.id ? `#${params.id}` : '.replayTable'; 48 | 49 | this.controlsContainer = d3.select(this.selector) 50 | .append('div') 51 | .attr('class', `controls-container ${params.visualizer}`); 52 | this.controls = this.renderControls(this.controlsContainer, this.params.controls); 53 | 54 | this.tableContainer = d3.select(this.selector) 55 | .append('div') 56 | .attr('class', `table-container ${params.visualizer}`); 57 | [this.table, this.rows, this.cells] = this.renderTable(this.data.results[this.currentRound].results); 58 | this.ys = this.rows.nodes().map(n => n.getBoundingClientRect().top); 59 | this.initialPositions = this.data.results[this.currentRound].results 60 | .reduce((obj, res) => Object.assign(obj, { [res.item]: res.position.strict - 1 }) , {}); 61 | } 62 | 63 | renderControls(container, list) { 64 | const controls = container.append('div') 65 | .attr('class', 'controls'); 66 | 67 | const roundMeta = this.data.results[this.currentRound].meta; 68 | 69 | const controlsObject = {}; 70 | const args = { 71 | play: [controls, roundMeta, this.play, this.pause], 72 | previous: [controls, roundMeta, this.previous], 73 | next: [controls, roundMeta, this.next], 74 | slider: [controls, this.data.meta.lastRound, this.roundsTotalNumber, roundMeta, this.preview, this.endPreview] 75 | }; 76 | list.forEach(control => controlsObject[control] = new Controls[control](...args[control])); 77 | 78 | Object.keys(controlsObject).forEach(ctrl => { 79 | const control = controlsObject[ctrl]; 80 | dispatchers.forEach(dispatcher => { 81 | const method = toCamelCase(`on-${dispatcher}`); 82 | if (control[method]) { 83 | this.dispatch.on(`${dispatcher}.${ctrl}`, control[method].bind(control)); 84 | } 85 | }); 86 | }); 87 | 88 | return controls; 89 | } 90 | 91 | move (roundIndex, delay, duration, cells = this.cells) { 92 | const nextPositions = this.data.results[roundIndex].results 93 | .reduce((obj, res) => Object.assign(obj, { [res.item]: res.position.strict - 1 }) , {}); 94 | 95 | return new Promise((resolve, reject) => { 96 | let transitionsFinished = 0; 97 | cells.transition() 98 | .delay(delay) 99 | .duration(duration) 100 | .style('transform', cell => { 101 | const initialY = this.ys[this.initialPositions[cell.result.item]]; 102 | const nextY = this.ys[nextPositions[cell.result.item]]; 103 | return `translateY(${nextY - initialY}px)`; 104 | }) 105 | .each(() => ++transitionsFinished) 106 | .on('end', () => { 107 | if (!--transitionsFinished) { 108 | resolve(); 109 | } 110 | }); 111 | }); 112 | } 113 | 114 | 115 | first () { 116 | return this.to(0); 117 | } 118 | 119 | last () { 120 | return this.to(this.data.meta.lastRound); 121 | } 122 | 123 | previous () { 124 | if (this.currentRound > 0) { 125 | return this.to(this.currentRound - 1); 126 | } 127 | } 128 | 129 | next () { 130 | if (this.currentRound < this.data.meta.lastRound) { 131 | return this.to(this.currentRound + 1); 132 | } 133 | } 134 | 135 | play (stopAt = this.data.meta.lastRound) { 136 | this.dispatch.call('play'); 137 | 138 | const playFunction = () => { 139 | if (this.currentRound === stopAt || !this.isPlaying) { 140 | this.pause(); 141 | } else { 142 | Promise.resolve(this.next()) 143 | .then(() => setTimeout(playFunction, this.durations.freeze)); 144 | } 145 | }; 146 | 147 | if (this.currentRound === this.data.meta.lastRound) { 148 | Promise.resolve(this.first()) 149 | .then(() => setTimeout(playFunction, this.durations.freeze)) 150 | } else { 151 | Promise.resolve(this.next()) 152 | .then(() => setTimeout(playFunction, this.durations.freeze)) 153 | } 154 | } 155 | 156 | pause () { 157 | this.dispatch.call('pause'); 158 | } 159 | 160 | endPreview (move = false) { 161 | const end = () => { 162 | this.dispatch.call('endPreview', this, this.data.results[this.currentRound].meta); 163 | return Promise.resolve(); 164 | }; 165 | 166 | if (this.previewedRound === null || this.previewedRound === this.currentRound) { 167 | return end(); 168 | } else if (!move) { 169 | return Promise.resolve(this.preview(this.currentRound)) 170 | .then(end); 171 | } else { 172 | return Promise.resolve(this.to(this.previewedRound)) 173 | .then(end); 174 | } 175 | } 176 | }; 177 | -------------------------------------------------------------------------------- /src/visualize/visualize.js: -------------------------------------------------------------------------------- 1 | import moduleConfig from './config'; 2 | import * as visualizersConfigs from './configs'; 3 | import initialize from '../configure/initialize'; 4 | import * as visualizers from './visualizers'; 5 | 6 | 7 | export default function (calculatedData, userConfig) { 8 | const params = initialize('visualizer', moduleConfig, visualizersConfigs, userConfig); 9 | return new visualizers[params.visualizer](calculatedData, params); 10 | }; 11 | -------------------------------------------------------------------------------- /src/visualize/visualizers/classic.js: -------------------------------------------------------------------------------- 1 | import Skeleton from '../skeleton'; 2 | import Cell from '../cell'; 3 | import fromCamelCase from '../../helpers/general/from-camel-case'; 4 | import getItemResults from '../../helpers/data/get-item-results'; 5 | 6 | 7 | const headerlessColumns = ['outcome', 'match', 'round', 'score', 'opponent']; 8 | 9 | export default class extends Skeleton { 10 | renderTable (data, classes = ['main'], columns = this.params.columns, labels = this.params.labels) { 11 | const table = this.tableContainer 12 | .append('table') 13 | .attr('class', classes.join(' ')); 14 | 15 | const thead = table.append('thead'); 16 | thead.append('tr') 17 | .selectAll('th') 18 | .data(columns) 19 | .enter().append('th') 20 | .text((column, i) => { 21 | if (labels[i]) { 22 | return labels[i]; 23 | } else if (headerlessColumns.includes(column) || column.includes('.change')) { 24 | return ''; 25 | } else { 26 | return fromCamelCase(column); 27 | } 28 | }); 29 | 30 | const tbody = table.append('tbody'); 31 | const rows = tbody.selectAll('tr') 32 | .data(data, k => k.item || k.roundMeta.index) 33 | .enter().append('tr'); 34 | 35 | const cells = rows.selectAll('td') 36 | .data(result => columns.map(column => new Cell(column, result, this.params))) 37 | .enter().append('td') 38 | .attr('class', cell => cell.classes.join(' ')) 39 | .style('background-color', cell => cell.backgroundColor || 'transparent') 40 | .text(cell => cell.text) 41 | .on('click', cell => { 42 | switch(cell.column) { 43 | case 'item': 44 | return this.drillDown(cell.result.item); 45 | case 'round': 46 | return this.endDrillDown(cell.result.roundMeta.index); 47 | default: 48 | return null; 49 | } 50 | }); 51 | 52 | return [table, rows, cells]; 53 | } 54 | 55 | to (roundIndex) { 56 | if (roundIndex < 0 || roundIndex > this.data.meta.lastRound) { 57 | return Promise.reject(`Sorry we can't go to round #${roundIndex}`); 58 | } 59 | 60 | this.dispatch.call('roundChange', this, this.data.results[roundIndex].meta); 61 | 62 | this.rows = this.rows 63 | .data(this.data.results[roundIndex].results, k => k.item); 64 | 65 | this.cells = this.cells 66 | .data(result => this.params.columns.map(column => new Cell(column, result, this.params))); 67 | 68 | const animateOutcomes = this.params.columns.includes('outcome'); 69 | if (animateOutcomes) { 70 | this.table.selectAll('td.outcome') 71 | .transition() 72 | .duration(this.durations.outcomes) 73 | .style("background-color", cell => this.params.colors[cell.result.outcome] || 'transparent'); 74 | } 75 | 76 | this.cells.filter('.change') 77 | .attr('class', cell => cell.classes.join(' ')) 78 | .text(cell => cell.text); 79 | 80 | return this.move(roundIndex, animateOutcomes ? this.durations.outcomes : 0, this.durations.move) 81 | .then(() => { 82 | this.cells.filter(':not(.change)') 83 | .attr('class', cell => cell.classes.join(' ')) 84 | .text(cell => cell.text); 85 | }); 86 | } 87 | 88 | preview (roundIndex) { 89 | this.dispatch.call('roundPreview', this, this.data.results[roundIndex].meta); 90 | 91 | this.rows = this.rows 92 | .data(this.data.results[roundIndex].results, k => k.item); 93 | 94 | this.cells = this.rows.selectAll('td') 95 | .data(result => this.params.columns.map(column => new Cell(column, result, this.params))) 96 | .attr('class', cell => cell.classes.join(' ')) 97 | .style('background-color', cell => cell.backgroundColor || 'transparent') 98 | .text(cell => cell.text); 99 | 100 | return Promise.resolve(); 101 | } 102 | 103 | drillDown (item) { 104 | this.dispatch.call('drillDown', this, item); 105 | 106 | this.controls.classed('hidden', true); 107 | this.drilldown.controls = this.controlsContainer.append('div') 108 | .attr('class', 'drilldown-contorls'); 109 | this.drilldown.controls.append('div') 110 | .attr('class', 'back') 111 | .text('<-') 112 | .on('click', this.endDrillDown.bind(this)); 113 | this.drilldown.controls.append('div') 114 | .attr('class', 'item') 115 | .text(item); 116 | 117 | const columns = ['round']; 118 | const labels = ['']; 119 | this.params.columns.forEach((column, i) => { 120 | const classes = new Cell(column, this.data.results[1].results[0], this.params).classes; 121 | if (column !== 'item' && !classes.includes('extra-item')) { 122 | columns.push(column); 123 | labels.push(this.params.labels[i] || ''); 124 | } 125 | }); 126 | 127 | const itemData = getItemResults(this.data.results, item, true); 128 | 129 | this.table.classed('hidden', true); 130 | [this.drilldown.table, this.drilldown.rows, this.drilldown.cells] = this.renderTable(itemData, ['drilldown'], columns, labels); 131 | 132 | return Promise.resolve(); 133 | } 134 | 135 | endDrillDown (roundIndex = null) { 136 | const end = () => { 137 | this.dispatch.call('endDrillDown', this, roundIndex); 138 | return Promise.resolve(); 139 | }; 140 | 141 | this.drilldown.controls.remove(); 142 | this.controls.classed('hidden', false); 143 | 144 | this.drilldown.table.remove(); 145 | this.table.classed('hidden', false); 146 | 147 | if (roundIndex !== null) { 148 | return Promise.resolve(this.to(roundIndex)) 149 | .then(end); 150 | } else { 151 | end(); 152 | } 153 | } 154 | }; 155 | -------------------------------------------------------------------------------- /src/visualize/visualizers/index.js: -------------------------------------------------------------------------------- 1 | export {default as classic} from './classic'; 2 | export {default as sparklines} from './sparklines'; 3 | -------------------------------------------------------------------------------- /src/visualize/visualizers/sparklines.js: -------------------------------------------------------------------------------- 1 | import Skeleton from '../skeleton'; 2 | import skeletonCell from '../cell'; 3 | import numberToChange from '../../helpers/general/number-to-change'; 4 | import isBetween from '../../helpers/general/is-between'; 5 | import getItemResults from '../../helpers/data/get-item-results'; 6 | import getSparkColor from '../helpers/sparklines/get-spark-color'; 7 | import getSparkClasses from '../helpers/sparklines/get-spark-classes'; 8 | 9 | 10 | const columns = { 11 | left: ['position', 'item'], 12 | right: ['score', 'opponent', 'points.change', 'equal', 'points', 'pointsLabel'], 13 | drilldown: ['score', 'opponent', 'wins', 'draws', 'losses', 'labeledPoints'] 14 | }; 15 | 16 | export default class extends Skeleton { 17 | constructor (data, params) { 18 | super(data, params); 19 | 20 | this.durations.scale = d3.scaleLinear() 21 | .domain([1, data.meta.lastRound]) 22 | .range([this.durations.move, 1.5*this.durations.move]); 23 | 24 | ['right', 'slider', 'sparks'].forEach(el => this[el].roundIndex = this.currentRound); 25 | } 26 | 27 | renderTable (data, classes = ['main']) { 28 | this.left = {}; 29 | this.sparks = {}; 30 | this.right = {}; 31 | this.slider = {}; 32 | 33 | this.left.columns = columns.left; 34 | this.right.columns = columns.right; 35 | 36 | [this.left.table, this.left.rows, this.left.cells] = this.makeTable(data, [...classes, 'left'], this.left.columns); 37 | [this.sparks.table, this.sparks.rows, this.sparks.cells] = this.makeSparks(data); 38 | [this.right.table, this.right.rows, this.right.cells] = this.makeTable(data, [...classes, 'right'], this.right.columns); 39 | 40 | this.sparks.width = this.sparks.rows.node().offsetWidth - this.sparks.cells.node().offsetWidth; 41 | 42 | this.scale = d3.scaleLinear() 43 | .domain([1, this.data.meta.lastRound]) 44 | .range([0, this.sparks.width]) 45 | .clamp(true); 46 | 47 | this.moveRightTable(this.currentRound); 48 | 49 | this.slider.top = this.makeSlider('top'); 50 | this.slider.bottom = this.makeSlider('bottom'); 51 | 52 | this.right.table.call(d3.drag() 53 | .on("start", () => { 54 | this.right.drag = { 55 | x: d3.event.x, 56 | roundIndex: this.right.roundIndex 57 | }; 58 | }) 59 | .on("drag", () => { 60 | const difference = Math.abs(this.right.drag.x - d3.event.x); 61 | const sign = Math.sign(this.right.drag.x - d3.event.x); 62 | const index = this.right.drag.roundIndex - sign*Math.round(this.scale.invert(difference)) + 1; 63 | const roundIndex = Math.min(Math.max(index, 1), this.data.meta.lastRound); 64 | 65 | this.moveRightTable(roundIndex); 66 | this.preview(roundIndex); 67 | }) 68 | .on("end", () => this.endPreview(true)) 69 | ); 70 | 71 | return ['table', 'rows', 'cells'].map(el => { 72 | const nodes = ['left', 'sparks', 'right'].map(part => this[part][el].nodes()); 73 | return d3.selectAll(d3.merge(nodes)); 74 | }); 75 | } 76 | 77 | makeTable (data, classes, columns) { 78 | const table = this.tableContainer 79 | .append('table') 80 | .attr('class', classes.join(' ')); 81 | 82 | const tbody = table.append('tbody'); 83 | const rows = tbody.selectAll('tr') 84 | .data(data, k => k.item) 85 | .enter().append('tr'); 86 | 87 | const cells = rows.selectAll('td') 88 | .data(result => columns.map(column => new Cell(column, result, this.params))) 89 | .enter().append('td') 90 | .attr('class', cell => cell.classes.join(' ')) 91 | .style('color', cell => cell.color) 92 | .text(cell => cell.text); 93 | 94 | cells.filter('.clickable') 95 | .on('click', cell => { 96 | switch(cell.column) { 97 | case 'item': 98 | if (this.drilldown.item !== cell.result.item) { 99 | return this.drillDown(cell.result.item); 100 | } else { 101 | return this.endDrillDown(); 102 | } 103 | default: 104 | return null; 105 | } 106 | }); 107 | 108 | return [table, rows, cells]; 109 | } 110 | 111 | makeSparks (data) { 112 | const table = this.tableContainer 113 | .append('table') 114 | .attr('class', 'sparks'); 115 | 116 | const tbody = table.append('tbody'); 117 | 118 | const sparksData = data.map(result => ({ 119 | item: result.item, 120 | results: getItemResults(this.data.results, result.item) 121 | })); 122 | 123 | const rows = tbody.selectAll('tr') 124 | .data(sparksData, k => k.item) 125 | .enter().append('tr'); 126 | 127 | const cells = rows.selectAll('td') 128 | .data(row => this.data.results.slice(1, this.data.meta.lastRound + 1).map((round, i) => ({ 129 | result: row.results[i+1], 130 | roundMeta: row.results[i+1].roundMeta 131 | }))) 132 | .enter().append('td') 133 | .attr('class', cell => getSparkClasses(cell, this.currentRound)) 134 | .style('background-color', cell => getSparkColor(cell, this.currentRound, this.params)) 135 | .on('mouseover', cell => this.preview(cell.roundMeta.index)) 136 | .on('mouseout', cell => this.endPreview(false)) 137 | .on('click', cell => this.endPreview(true)); 138 | 139 | const scale = d3.scaleLinear() 140 | .domain([1, sparksData.length]) 141 | .range([0, 100]); 142 | 143 | cells.filter(cell => cell.result.change !== null) 144 | .append('span') 145 | .attr('class', 'spark-position') 146 | .style('top', cell => `${scale(cell.result.position.strict)}%`); 147 | 148 | cells.filter(cell => cell.result.change !== null) 149 | .append('span') 150 | .attr('class', 'spark-score muted') 151 | .style('color', cell => this.params.colors[cell.result.outcome] || 'black') 152 | .text(cell => cell.result.match ? `${cell.result.match.score}:${cell.result.match.opponentScore}` : ''); 153 | 154 | cells.filter(cell => cell.roundMeta.index > this.currentRound) 155 | .classed('overlapped', true); 156 | 157 | 158 | this.dispatch.on('roundPreview.sparks', roundMeta => this.moveSparks(roundMeta.index, 0)); 159 | 160 | return [table, rows, cells]; 161 | } 162 | 163 | makeSlider (position = 'top') { 164 | const slider = position === 'top' 165 | ? this.sparks.table.select('tbody').insert('tr', 'tr') 166 | : this.sparks.table.select('tbody').append('tr'); 167 | 168 | slider 169 | .attr('class', `sparklines-slider ${position}`) 170 | .append('td') 171 | .attr('class', 'slider-cell') 172 | .attr('colspan', this.roundsTotalNumber); 173 | 174 | const left = `${this.scale(this.currentRound)}px`; 175 | return slider.select('.slider-cell') 176 | .append('span') 177 | .attr('class', 'slider-toggle') 178 | .style('left', left) 179 | .text(this.data.results[this.currentRound].meta.name) 180 | .call(d3.drag() 181 | .on("drag", () => { 182 | const roundIndex = Math.round(this.scale.invert(d3.event.x)); 183 | this.moveRightTable(roundIndex); 184 | this.preview(roundIndex); 185 | }) 186 | .on("end", () => this.endPreview(true)) 187 | ); 188 | } 189 | 190 | to (roundIndex) { 191 | if (roundIndex < 1 || roundIndex > this.data.meta.lastRound) { 192 | return Promise.reject(`Sorry we can't go to round #${roundIndex}`); 193 | } 194 | 195 | if (roundIndex === this.currentRound) { 196 | return Promise.resolve(); 197 | } 198 | 199 | const change = roundIndex - this.currentRound; 200 | this.dispatch.call('roundChange', this, this.data.results[roundIndex].meta); 201 | 202 | ['left', 'right'].forEach(side => { 203 | this[side].rows 204 | .data(this.data.results[roundIndex].results, k => k.item); 205 | 206 | this[side].cells = this[side].cells 207 | .data(result => this[side].columns.map(column => new Cell(column, result, this.params))); 208 | }); 209 | 210 | this.right.cells.filter('.change') 211 | .attr('class', cell => cell.classes.join(' ')) 212 | .style('color', cell => cell.color) 213 | .text(cell => cell.text); 214 | 215 | const preAnimations = ['right', 'slider', 'sparks'] 216 | .filter(element => this[element].roundIndex !== this.currentRound); 217 | 218 | preAnimations.forEach(element => { 219 | return { 220 | right: this.moveRightTable, 221 | slider: this.moveSlider, 222 | sparks: this.moveSparks 223 | }[element].bind(this)(roundIndex, this.durations.pre) 224 | }); 225 | 226 | const duration = this.durations.scale(Math.abs(change)); 227 | return this.move(roundIndex, preAnimations.length ? this.durations.pre : 0, duration) 228 | .then(() => { 229 | const merged = d3.merge([this.left.cells.nodes(), this.right.cells.filter(':not(.change)').nodes()]); 230 | d3.selectAll(merged) 231 | .attr('class', cell => cell.classes.join(' ')) 232 | .style('color', cell => cell.color) 233 | .text(cell => cell.text); 234 | }); 235 | } 236 | 237 | moveSlider (roundIndex, duration = 0) { 238 | const left =`${this.scale(roundIndex)}px`; 239 | [this.slider.top, this.slider.bottom].map(slider => { 240 | slider 241 | .transition() 242 | .duration(duration) 243 | .style('left', left) 244 | .text(this.data.results[roundIndex].meta.name) 245 | .on('end', () => this.slider.roundIndex = roundIndex); 246 | }); 247 | } 248 | 249 | moveRightTable (roundIndex, duration = 0) { 250 | this.right.table 251 | .transition() 252 | .duration(duration) 253 | .style('left', `-${this.sparks.width - this.scale(roundIndex)}px`) 254 | .on('end', () => this.right.roundIndex = roundIndex); 255 | } 256 | 257 | moveSparks (roundIndex, duration = 0) { 258 | const changed = this.sparks.cells 259 | .filter(cell => isBetween(cell.roundMeta.index, roundIndex, this.sparks.roundIndex)); 260 | 261 | if (!duration) { 262 | changed 263 | .style('background-color', cell => getSparkColor(cell, roundIndex, this.params)) 264 | .style('opacity', cell => cell.roundMeta.index > roundIndex ? 0.15 : 1); 265 | 266 | this.sparks.roundIndex = roundIndex 267 | } else { 268 | changed 269 | .transition() 270 | .duration(duration) 271 | .style('background-color', cell => getSparkColor(cell, roundIndex, this.params)) 272 | .style('opacity', cell => cell.roundMeta.index > roundIndex ? 0.15 : 1) 273 | .on('end', () => this.sparks.roundIndex = roundIndex); 274 | } 275 | } 276 | 277 | first () { 278 | return this.to(1); 279 | } 280 | 281 | preview (roundIndex) { 282 | if (roundIndex < 1 || roundIndex > this.data.meta.lastRound) { 283 | return Promise.reject(`Sorry we can't preview round #${roundIndex}`); 284 | } 285 | 286 | const previousPreviewedRound = this.previewedRound; 287 | 288 | if (previousPreviewedRound === roundIndex) { 289 | return Promise.resolve(); 290 | } 291 | 292 | this.dispatch.call('roundPreview', this, this.data.results[roundIndex].meta); 293 | 294 | this.moveSlider(roundIndex); 295 | 296 | ['left', 'right'].forEach(side => { 297 | this[side].rows 298 | .data(this.data.results[roundIndex].results, k => k.item); 299 | 300 | this[side].cells = this[side].cells 301 | .data(result => this[side].columns.map(column => new Cell(column, result, this.params))) 302 | .attr('class', cell => cell.classes.join(' ')) 303 | .style('color', cell => cell.color) 304 | .text(cell => cell.text); 305 | }); 306 | 307 | return Promise.resolve(); 308 | } 309 | 310 | drillDown (item) { 311 | this.dispatch.call('drillDown', this, item); 312 | 313 | if (!this.drilldown.controls) { 314 | this.drilldown.controls = this.controls.append('div') 315 | .attr('class', 'drilldown-control') 316 | .on('click', this.endDrillDown) 317 | .text(this.params.allLabel); 318 | } 319 | 320 | this.right.columns = columns.drilldown; 321 | 322 | this.right.cells 323 | .data(result => this.right.columns.map(column => new Cell(column, result, this.params))) 324 | .attr('class', cell => cell.classes.join(' ')) 325 | .style('color', cell => cell.color) 326 | .text(cell => cell.text); 327 | 328 | this.right.rows.classed('muted', row => row.item !== item); 329 | 330 | this.sparks.cells 331 | .classed('muted', cell => !cell.result.match || (cell.result.item !== item && cell.result.match.opponent !== item)); 332 | 333 | this.sparks.cells.selectAll('.spark-score') 334 | .classed('muted', cell => !cell.result.match || cell.result.item === item || cell.result.match.opponent !== item); 335 | 336 | return Promise.resolve(); 337 | } 338 | 339 | endDrillDown () { 340 | this.drilldown.controls.remove(); 341 | this.drilldown.controls = null; 342 | 343 | this.sparks.cells.classed('muted', false); 344 | 345 | this.sparks.cells.selectAll('.spark-score') 346 | .classed('muted', true); 347 | 348 | this.right.columns = columns.right; 349 | 350 | this.right.cells 351 | .data(result => this.right.columns.map(column => new Cell(column, result, this.params))) 352 | .attr('class', cell => cell.classes.join(' ')) 353 | .style('color', cell => cell.color) 354 | .text(cell => cell.text); 355 | 356 | this.right.rows.classed('muted', false); 357 | 358 | this.dispatch.call('endDrillDown', this, null); 359 | 360 | return Promise.resolve(); 361 | } 362 | }; 363 | 364 | 365 | class Cell extends skeletonCell { 366 | score (result, params) { 367 | this.text = result.match && result.match.score !== null ? `${result.match.score}:${result.match.opponentScore}` : ''; 368 | this.classes = ['score', 'change']; 369 | this.color = params.colors[result.outcome]; 370 | return this; 371 | } 372 | 373 | opponent (result, params) { 374 | this.text = result.match ? result.match.opponent : ''; 375 | this.classes = ['opponent', 'change']; 376 | return this; 377 | } 378 | 379 | equal (result, params) { 380 | this.text = result.position.strict === 1 ? '=' : ''; 381 | this.classes = ['label']; 382 | return this; 383 | } 384 | 385 | pointsLabel (result, params) { 386 | this.text = result.position.strict === 1 ? params.pointsLabel : ''; 387 | this.classes = ['label']; 388 | return this; 389 | } 390 | 391 | wins (result, params) { 392 | this.text = `${result.wins.total} ${params.shortOutcomeLabels.win}`; 393 | this.classes = ['change']; 394 | this.color = params.colors.win; 395 | return this; 396 | } 397 | 398 | draws (result, params) { 399 | this.text = `${result.draws.total} ${params.shortOutcomeLabels.draw}`; 400 | this.classes = ['calculation']; 401 | this.color = params.colors.draw; 402 | return this; 403 | } 404 | 405 | losses (result, params) { 406 | this.text = `${result.losses.total} ${params.shortOutcomeLabels.loss}`; 407 | this.classes = ['calculation']; 408 | this.color = params.colors.loss; 409 | return this; 410 | } 411 | 412 | labeledPoints (result, params) { 413 | this.text = `${result.points.total} ${params.pointsLabel}`; 414 | this.classes = ['calculation']; 415 | return this; 416 | } 417 | 418 | makeChange (column, result, params) { 419 | const calc = column.replace('.change', ''); 420 | this.text = result.change !== null ? numberToChange(result[calc].change, '0') : ''; 421 | this.classes = ['change']; 422 | this.color = params.colors[result.outcome]; 423 | return this; 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const path = require('path'); 4 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 5 | const UnminifiedWebpackPlugin = require('unminified-webpack-plugin'); 6 | 7 | const config = { 8 | entry: './src/replay-table.js', 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | publicPath: '', 12 | filename: 'replay-table.min.js', 13 | library: 'replayTable', 14 | libraryTarget: 'umd' 15 | }, 16 | module: { 17 | rules: [ 18 | {test: /\.(js|jsx)$/, exclude: /node_modules/, use: 'babel-loader'}, 19 | {test: /\.css$/, use: ExtractTextPlugin.extract({fallback: 'style-loader', use: 'css-loader'})} 20 | ] 21 | }, 22 | externals: { 23 | d3: { 24 | commonjs: 'd3', 25 | commonjs2: 'd3', 26 | amd: 'd3', 27 | root: 'd3' 28 | } 29 | }, 30 | plugins: [ 31 | new webpack.optimize.UglifyJsPlugin(), 32 | new HtmlWebpackPlugin({ 33 | template: 'index.html', 34 | inject: 'head' 35 | }), 36 | new ExtractTextPlugin("replay-table.css"), 37 | new webpack.DefinePlugin({'process.env': {NODE_ENV: JSON.stringify('production')}}), 38 | new UnminifiedWebpackPlugin() 39 | ] 40 | }; 41 | 42 | module.exports = config; 43 | --------------------------------------------------------------------------------