├── .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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------