├── README.md ├── favicon.ico ├── spaces ├── github.svg ├── matches.tsv ├── LICENSE ├── annotations.json ├── index.html ├── style.css ├── swoopy-drag.js └── script.js /README.md: -------------------------------------------------------------------------------- 1 | # worlds-2019 2 | https://rocket3989.github.io/worlds-2019/ 3 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket3989/worlds-2019/HEAD/favicon.ico -------------------------------------------------------------------------------- /spaces: -------------------------------------------------------------------------------- 1 | [master cd8fe61] tabs - 2 | 2 files changed, 40 insertions(+), 40 deletions(-) 3 | create mode 100644 spaces 4 | Already up to date. 5 | 6 | -------------------------------------------------------------------------------- /github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /matches.tsv: -------------------------------------------------------------------------------- 1 | group t1 t2 winner date 2 | C FNC SKT 2 10-12 3 | C RNG CG 1 10-12 4 | D IG AHQ 1 10-12 5 | D DWG TL 2 10-12 6 | B JT FPX 1 10-12 7 | B GAM SPY 2 10-12 8 | C SKT RNG 1 10-13 9 | C FNC CG 1 10-13 10 | D TL IG 2 10-13 11 | D AHQ DWG 2 10-13 12 | A GRF G2 2 10-13 13 | A C9 HKA 1 10-13 14 | D IG DWG 2 10-14 15 | D AHQ TL 2 10-14 16 | B FPX SPY 1 10-14 17 | B JT GAM 2 10-14 18 | A GRF HKA 1 10-14 19 | A G2 C9 1 10-14 20 | C RNG FNC 1 10-15 21 | C CG SKT 2 10-15 22 | B FPX GAM 1 10-15 23 | B SPY JT 2 10-15 24 | A C9 GRF 2 10-15 25 | A HKA G2 2 10-15 26 | B GAM FPX TBD 10-17 27 | B JT SPY TBD 10-17 28 | B GAM JT TBD 10-17 29 | B SPY FPX TBD 10-17 30 | B SPY GAM TBD 10-17 31 | B FPX JT TBD 10-17 32 | A GRF C9 TBD 10-18 33 | A G2 HKA TBD 10-18 34 | A HKA GRF TBD 10-18 35 | A C9 G2 TBD 10-18 36 | A HKA C9 TBD 10-18 37 | A G2 GRF TBD 10-18 38 | C RNG SKT TBD 10-19 39 | C CG FNC TBD 10-19 40 | C SKT FNC TBD 10-19 41 | C CG RNG TBD 10-19 42 | C SKT CG TBD 10-19 43 | C FNC RNG TBD 10-19 44 | D AHQ IG TBD 10-20 45 | D TL DWG TBD 10-20 46 | D TL AHQ TBD 10-20 47 | D DWG IG TBD 10-20 48 | D DWG AHQ TBD 10-20 49 | D IG TL TBD 10-20 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 rocket3989 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 | -------------------------------------------------------------------------------- /annotations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "M -36,279 A 45.026 45.026 0 0 0 43,310", 4 | "text": "Tap to see how the first match affects the group", 5 | "team": "C9", 6 | "lw": 21, 7 | "textOffset": [ 8 | -47, 9 | 244 10 | ] 11 | }, 12 | { 13 | "path": "M -15,-69 A 105.02 105.02 0 0 0 -3,39", 14 | "text": "C9 needs to beat GRF for the best chance out", 15 | "team": "C9", 16 | "lw": 22, 17 | "textOffset": [ 18 | -47, 19 | -94 20 | ] 21 | }, 22 | { 23 | "path": "M -2000 0", 24 | "text": "Two wins advance JT or FPX out of groups", 25 | "team": "FPX", 26 | "lw": 22, 27 | "textOffset": [ 28 | -107, 29 | 12 30 | ] 31 | }, 32 | { 33 | "path": "M -2000 0", 34 | "text": "Two loses knock GAM and SPY out", 35 | "team": "SPY", 36 | "lw": 22, 37 | "textOffset": [ 38 | -107, 39 | 12 40 | ] 41 | }, 42 | { 43 | "path": "M 261,-64 A 62.002 62.002 0 0 1 182,4", 44 | "text": "Even with three wins, CG can still be eliminated", 45 | "team": "CG", 46 | "lw": 24, 47 | "textOffset": [ 48 | 183, 49 | -88 50 | ] 51 | }, 52 | { 53 | "path": "M 261,-64 A 62.002 62.002 0 0 1 182,4", 54 | "text": "AHQ needs to go 3-0 and win a tiebreaker to advance", 55 | "team": "AHQ", 56 | "lw": 24, 57 | "textOffset": [ 58 | 183, 59 | -88 60 | ] 61 | }, 62 | { 63 | "path": "M 286,220 A 123.355 123.355 0 0 1 105,221", 64 | "text": "G2 can lose all three games and still advance without a tiebreaker", 65 | "team": "G2", 66 | "lw": 24, 67 | "textOffset": [ 68 | 241, 185 69 | ] 70 | }, 71 | { 72 | "path": "M -26,-55 A 179.253 179.253 0 0 0 -6,62", 73 | "text": "CG has a slim chance of getting a tiebreaker without beating SKT", 74 | "team": "CG", 75 | "lw": 24, 76 | "textOffset": [ 77 | -92,-90 78 | ] 79 | }, 80 | { 81 | "path": "M -2000", 82 | "text": "With each of the top three teams holding a win over another, Group D is poised to have an exciting finish", 83 | "team": "TL", 84 | "lw": 24, 85 | "textOffset": [ 86 | 234,-137 87 | ] 88 | } 89 | ] -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 19 | 2019 Worlds Group Advancement 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | 36 | 37 | 38 |
39 |
40 | Adam & Collin Pearce 41 | 42 | 43 | 44 | 45 |
46 |
47 | 48 |

2019 Worlds Group Advancement

49 |

The second half of League of Legends’ World Group Stage start in just one day! Each team will play the other three teams in their group once more. The best two teams in each group advance to quarterfinals.

50 |

The charts below show how each team could advance. With six games left in each group, there are 26 = 64 ways for group play to end. Each of these 64 outcomes is represented by a circle under each team. Green circles show scenarios in which a team advances, and red circles show elimination. Yellow circles indicate scenarios with a tiebreaker to decide advancement. Clicking on a match below the group will select an outcome of that match, filtering out other scenarios.

51 |

With only one possible elimination scenario each, G2 and SKT have the best positioning out of all teams.

52 | 53 |

Group A

54 |
55 |

Group B

56 |
57 |

Group C

58 |
59 |

Group D

60 |
61 | 62 | 63 |

code 64 | 2019 MSI 65 | 2018 Worlds 66 | 2018 MSI 67 | 2017 Worlds 68 | 2017 MSI 69 | 2016 Worlds

70 | 71 | 72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 | 80 | 81 |
82 | 83 |
84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | button { 2 | display: inline-block; 3 | border: none; 4 | padding: 5px; 5 | margin: 0px 5px; 6 | background: rgb(218, 218, 218); 7 | color: #000000; 8 | font-family: sans-serif; 9 | font-size: .9rem; 10 | cursor: pointer; 11 | text-align: center; 12 | transition: background 250ms ease-in-out, 13 | transform 150ms ease; 14 | -webkit-appearance: none; 15 | -moz-appearance: none; 16 | } 17 | 18 | button:hover, 19 | button:focus { 20 | background: rgb(194, 194, 194); 21 | } 22 | 23 | 24 | button:active { 25 | transform: scale(0.99); 26 | } 27 | 28 | svg{ 29 | font-family: monospace; 30 | overflow: visible; 31 | font-size: 14px; 32 | } 33 | 34 | svg text{ 35 | -webkit-text-shadow: 0 1px 0 #F5F5F5, 1px 0 0 #F5F5F5, 0 -1px 0 #F5F5F5, -1px 0 0 #F5F5F5; 36 | text-shadow: 0 1px 0 #F5F5F5, 1px 0 0 #F5F5F5, 0 -1px 0 #F5F5F5, -1px 0 0 #F5F5F5; 37 | 38 | } 39 | 40 | html { 41 | min-width: 760px; 42 | background-color: #F5F5F5; 43 | font-weight: normal; 44 | margin: auto 0px ; 45 | 46 | 47 | } 48 | 49 | h1{ 50 | text-align: center; 51 | } 52 | 53 | div.matches{ 54 | -webkit-user-select: none; 55 | -moz-user-select: none; 56 | -khtml-user-select: none; 57 | -ms-user-select: none; 58 | } 59 | 60 | .annotations{ 61 | pointer-events: all; 62 | } 63 | 64 | 65 | .group{ 66 | margin: 0px auto; 67 | padding-left: 10px; 68 | display: inline-block; 69 | } 70 | 71 | .group-header{ 72 | text-align: center; 73 | margin-bottom: -0px; 74 | margin-top: 80px; 75 | opacity: 0; 76 | } 77 | 78 | sup, sub { 79 | vertical-align: baseline; 80 | position: relative; 81 | top: -0.4em; 82 | } 83 | sub { 84 | top: 0.4em; 85 | } 86 | 87 | .team{ 88 | display: inline-block; 89 | width: 300px; 90 | padding-left: 30px; 91 | padding-right: 30px; 92 | position: relative; 93 | z-index: 0; 94 | } 95 | 96 | .active{ 97 | stroke: black; 98 | stroke-width: 4; 99 | } 100 | 101 | .hidden{ 102 | opacity: .2; 103 | } 104 | 105 | 106 | div.tooltip { 107 | top: -1000px; 108 | position: fixed; 109 | padding: 10px; 110 | background: rgba(255, 255, 255, .90); 111 | border: 1px solid lightgray; 112 | pointer-events: none; 113 | font-family: monospace; 114 | width: 200px; 115 | 116 | 117 | } 118 | .tooltip > .game{ 119 | display: inline-block; 120 | width: 100px; 121 | height: 25px; 122 | } 123 | .tooltip-hidden{ 124 | opacity: 0; 125 | transition: all .3s; 126 | transition-delay: .1s; 127 | } 128 | .tooltip.game{ 129 | width: 50px; 130 | } 131 | 132 | 133 | .game .won{ 134 | font-weight: 700; 135 | text-decoration: underline; 136 | } 137 | 138 | .matches{ 139 | font-size: 16px; 140 | text-align: center; 141 | margin-left: 0px; 142 | } 143 | 144 | .matches .game{ 145 | display: inline-block; 146 | margin-bottom: 10px; 147 | width: 100px; 148 | cursor: pointer; 149 | border: 1px solid black; 150 | margin-right: 10px; 151 | opacity: .6; 152 | font-family: monospace; 153 | } 154 | .matches .game.active{ 155 | opacity: 1; 156 | } 157 | 158 | 159 | .annotations text{ 160 | font-size: 12px; 161 | } 162 | html{ 163 | background-color: #f5f5f5; 164 | font-weight: normal; 165 | } 166 | 167 | body{ 168 | max-width: 750px; 169 | margin: 0px auto; 170 | font-family: 'Roboto Slab', serif; 171 | line-height: 1.55em; 172 | font-size: 16px; 173 | margin-top: 5px; 174 | margin-bottom: 80px; 175 | } 176 | 177 | @media(max-width: 760px){ 178 | body{ 179 | padding: 5px; 180 | margin: 0px 8px; 181 | } 182 | .matches{ 183 | margin-left: 20px; 184 | } 185 | } 186 | 187 | p{ 188 | line-height: 1.55em; 189 | } 190 | 191 | a{ 192 | color: black; 193 | } 194 | 195 | .header{ 196 | position: relative; 197 | color: black; 198 | font-size: 16px; 199 | height: 24px; 200 | overflow: visible; 201 | /*font-family: Consolas, monaco, 'Lucida Console', monospace; */ 202 | /*font-family: 'Roboto', serif;*/ 203 | font-family: 'Roboto Slab', serif; 204 | } 205 | .header-left{ 206 | float: left; 207 | } 208 | .header-right{ 209 | float: right; 210 | } 211 | .header span{ 212 | opacity: .5; 213 | } 214 | 215 | .header a{ 216 | opacity: .5; 217 | text-decoration: none; 218 | } 219 | .header a:hover{ 220 | opacity: 1 221 | } 222 | .header img{ 223 | width: 16px; 224 | margin-right: 2px; 225 | position: relative; 226 | top: 2px; 227 | } 228 | 229 | 230 | h1,h2,h3,h4,h5{ 231 | font-family: 'Roboto', sans-serif; 232 | margin-top: 1.5em; 233 | margin-bottom: .5em; 234 | } 235 | h1{ 236 | font-weight: 500; 237 | font-size: 34px; 238 | margin-bottom: 1.2em; 239 | line-height: 1.3em; 240 | margin-top: 1.2em; 241 | } 242 | h2,h3,h4,h5{ 243 | font-size: 22px; 244 | } 245 | 246 | :not(pre) > code{ 247 | background: #dedede; 248 | padding: .05em; 249 | padding-left: .2em; 250 | padding-right: .2em; 251 | border-radius: 2px; 252 | font-family: monospace; 253 | } 254 | 255 | strong{ 256 | font-weight: bold; 257 | } 258 | 259 | .annotations path{ 260 | fill: none; 261 | stroke: #000; 262 | stroke-width: .6px; 263 | } 264 | 265 | @media (prefers-color-scheme: dark) { 266 | html{ 267 | background-color: #222; 268 | color: #aaa 269 | } 270 | svg text{ 271 | /*-webkit-text-stroke: 4px navy;*/ 272 | -webkit-text-shadow: 0 1px 0 #222, 1px 0 0 #222, 0 -1px 0 #222, -1px 0 0 #222; 273 | text-shadow: 0 1px 0 #222, 1px 0 0 #222, 0 -1px 0 #222, -1px 0 0 #222; 274 | color: #aaa 275 | 276 | } 277 | text{ 278 | color: #aaa; 279 | fill: #aaa; 280 | } 281 | ​path { 282 | stroke: #aaa; 283 | }​ 284 | .annotations path { 285 | fill: none; 286 | stroke: #aaa; 287 | } 288 | marker { 289 | stroke: #aaa; 290 | fill: #aaa 291 | } 292 | a{ 293 | color: #aaa; 294 | stroke: #aaa; 295 | fill: #aaa 296 | } 297 | body{ 298 | color: #aaa; 299 | } 300 | 301 | div.tooltip { 302 | background:#222e; 303 | } 304 | .matches .game{ 305 | border: 1px solid #aaa; 306 | } 307 | .header{ 308 | color: #aaa; 309 | } 310 | .scenario.active{ 311 | stroke: #aaa; 312 | stroke-width: 3; 313 | 314 | } 315 | } -------------------------------------------------------------------------------- /swoopy-drag.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3')) : 3 | typeof define === 'function' && define.amd ? define(['exports', 'd3'], factory) : 4 | (factory((global.d3 = global.d3 || {}),global.d3)); 5 | }(this, function (exports,d3) { 'use strict'; 6 | 7 | function swoopyDrag(){ 8 | var x = function(d){ return d } 9 | var y = function(d){ return d } 10 | 11 | var annotations = [] 12 | var annotationSel 13 | 14 | var draggable = false 15 | 16 | var dispatch = d3.dispatch('drag') 17 | 18 | var textDrag = d3.drag() 19 | .on('drag', function(d){ 20 | var x = d3.event.x 21 | var y = d3.event.y 22 | d.textOffset = [x, y].map(Math.round) 23 | 24 | d3.select(this).call(translate, d.textOffset) 25 | 26 | dispatch.call('drag') 27 | }) 28 | .subject(function(d){ return {x: d.textOffset[0], y: d.textOffset[1]} }) 29 | 30 | var circleDrag = d3.drag() 31 | .on('drag', function(d){ 32 | var x = d3.event.x 33 | var y = d3.event.y 34 | d.pos = [x, y].map(Math.round) 35 | 36 | var parentSel = d3.select(this.parentNode) 37 | 38 | var path = '' 39 | var points = parentSel.selectAll('circle').data() 40 | if (points[0].type == 'A'){ 41 | path = calcCirclePath(points) 42 | } else{ 43 | points.forEach(function(d){ path = path + d.type + d.pos }) 44 | } 45 | 46 | parentSel.select('path').attr('d', path).datum().path = path 47 | d3.select(this).call(translate, d.pos) 48 | 49 | dispatch.call('drag') 50 | }) 51 | .subject(function(d){ return {x: d.pos[0], y: d.pos[1]} }) 52 | 53 | 54 | var rv = function(sel){ 55 | annotationSel = sel.html('').selectAll('g') 56 | .data(annotations).enter() 57 | .append('g') 58 | .call(translate, function(d){ return [x(d), y(d)] }) 59 | 60 | var textSel = annotationSel.append('text') 61 | .call(translate, ƒ('textOffset')) 62 | .text(ƒ('text')) 63 | 64 | annotationSel.append('path') 65 | .attr('d', ƒ('path')) 66 | 67 | if (!draggable) return 68 | 69 | annotationSel.style('cursor', 'pointer') 70 | textSel.call(textDrag) 71 | 72 | annotationSel.selectAll('circle').data(function(d){ 73 | var points = [] 74 | 75 | if (~d.path.indexOf('A')){ 76 | //handle arc paths seperatly -- only one circle supported 77 | var pathNode = d3.select(this).select('path').node() 78 | var l = pathNode.getTotalLength() 79 | 80 | points = [0, .5, 1].map(function(d){ 81 | var p = pathNode.getPointAtLength(d*l) 82 | return {pos: [p.x, p.y], type: 'A'} 83 | }) 84 | } else{ 85 | var i = 1 86 | var type = 'M' 87 | var commas = 0 88 | 89 | for (var j = 1; j < d.path.length; j++){ 90 | var curChar = d.path[j] 91 | if (curChar == ',') commas++ 92 | if (curChar == 'L' || curChar == 'C' || commas == 2){ 93 | points.push({pos: d.path.slice(i, j).split(','), type: type}) 94 | type = curChar 95 | i = j + 1 96 | commas = 0 97 | } 98 | } 99 | 100 | points.push({pos: d.path.slice(i, j).split(','), type: type}) 101 | } 102 | 103 | return points 104 | }).enter().append('circle') 105 | .attr('r', 8) 106 | .attr('fill', 'rgba(0,0,0,0)') 107 | .attr('stroke', '#333') 108 | .attr('stroke-dasharray', '2 2') 109 | .call(translate, ƒ('pos')) 110 | .call(circleDrag) 111 | 112 | dispatch.call('drag') 113 | } 114 | 115 | 116 | rv.annotations = function(_x){ 117 | if (typeof(_x) == 'undefined') return annotations 118 | annotations = _x 119 | return rv 120 | } 121 | rv.x = function(_x){ 122 | if (typeof(_x) == 'undefined') return x 123 | x = _x 124 | return rv 125 | } 126 | rv.y = function(_x){ 127 | if (typeof(_x) == 'undefined') return y 128 | y = _x 129 | return rv 130 | } 131 | rv.draggable = function(_x){ 132 | if (typeof(_x) == 'undefined') return draggable 133 | draggable = _x 134 | return rv 135 | } 136 | rv.on = function() { 137 | var value = dispatch.on.apply(dispatch, arguments); 138 | return value === dispatch ? rv : value; 139 | } 140 | 141 | return rv 142 | 143 | //convert 3 points to an Arc Path 144 | function calcCirclePath(points){ 145 | var a = points[0].pos 146 | var b = points[2].pos 147 | var c = points[1].pos 148 | 149 | var A = dist(b, c) 150 | var B = dist(c, a) 151 | var C = dist(a, b) 152 | 153 | var angle = Math.acos((A*A + B*B - C*C)/(2*A*B)) 154 | 155 | //calc radius of circle 156 | var K = .5*A*B*Math.sin(angle) 157 | var r = A*B*C/4/K 158 | r = Math.round(r*1000)/1000 159 | 160 | //large arc flag 161 | var laf = +(Math.PI/2 > angle) 162 | 163 | //sweep flag 164 | var saf = +((b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0]) < 0) 165 | 166 | return ['M', a, 'A', r, r, 0, laf, saf, b].join(' ') 167 | } 168 | 169 | function dist(a, b){ 170 | return Math.sqrt( 171 | Math.pow(a[0] - b[0], 2) + 172 | Math.pow(a[1] - b[1], 2)) 173 | } 174 | 175 | 176 | //no jetpack dependency 177 | function translate(sel, pos){ 178 | sel.attr('transform', function(d){ 179 | var posStr = typeof(pos) == 'function' ? pos(d) : pos 180 | return 'translate(' + posStr + ')' 181 | }) 182 | } 183 | 184 | function ƒ(str){ return function(d){ return d[str] } } 185 | } 186 | 187 | exports.swoopyDrag = swoopyDrag; 188 | 189 | Object.defineProperty(exports, '__esModule', { value: true }); 190 | 191 | })); -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // http://esports-assets.s3.amazonaws.com/production/files/rules/2017-World-Championship-Rules-v-17-3.pdf 2 | // https://esports-assets.s3.amazonaws.com/production/files/rules/2017-MSI-Ruleset.pdf 3 | var byGroup 4 | var colorChoice = 0 5 | var staticPick = false; 6 | var ƒ = d3.f 7 | // console.clear() 8 | function readFile(){ 9 | d3.loadData('annotations.json', 'matches.tsv', function(err, res){ 10 | d3.selectAll('.group-header').st({opacity: 1}) 11 | 12 | annotations = res[0] 13 | matches = res[1] 14 | 15 | teams2wins = {} 16 | 17 | matches.forEach(function(d, i){ 18 | d.winner = +d.winner 19 | if( i < 24 || !staticPick){ 20 | d.wName = d['t' + d.winner] 21 | d.actualWinner = !(d.winner === 0) ? d.winner : 3 22 | } 23 | d.complete = i < 24 24 | d.allTeams = d.t1 + '-' + d.t2 25 | if (!teams2wins[d.t1]) teams2wins[d.t1] = 0 26 | if (!teams2wins[d.t2]) teams2wins[d.t2] = 0 27 | if (d.date < "10-17") teams2wins[d.wName]++ //make sure to change key each year 28 | }) 29 | 30 | byGroup = d3.nestBy(matches, ƒ('group')) 31 | reDraw(); 32 | }) 33 | } 34 | readFile() 35 | 36 | function static(){ 37 | staticPick = !staticPick 38 | readFile(); 39 | } 40 | 41 | function changeColor(){ 42 | colorChoice ^= 1; 43 | reDraw(); 44 | } 45 | 46 | function changeSeed(){ 47 | colorChoice ^= 2; 48 | reDraw(); 49 | } 50 | 51 | function reDraw(){ 52 | byGroup.forEach(drawGroup) 53 | } 54 | 55 | 56 | 57 | function scoreMatches(matches){ 58 | var teams = d3.nestBy(matches, ƒ('t1')).map(function(d){ 59 | return {name: d.key, w: 0} 60 | }) 61 | var nameToTeam = {} 62 | teams.forEach(function(d){ nameToTeam[d.name] = d }) 63 | matches.forEach(addMatchWins) 64 | 65 | teams.forEach(function(d){ 66 | d.wins = d.w 67 | d.w = 0 68 | }) 69 | 70 | d3.nestBy(teams, ƒ('wins')).forEach(function(d){ 71 | if (d.length == 1 || d.length == 4) return 72 | 73 | 74 | var tiedTeams = d.map(ƒ('name')).join('-') 75 | var tiedMatches = matches 76 | .filter(function(d){ 77 | return ~tiedTeams.indexOf(d.t1) && ~tiedTeams.indexOf(d.t2) }) 78 | tiedMatches.forEach(addMatchWins) 79 | 80 | // in 3-way tie, only head2head winning record gets out of tiebreaker 81 | if (d.length != 3) return 82 | // console.log(d.length, d.map(d => d.w).join(' ')) 83 | // console.log(d.length, JSON.parse(JSON.stringify(d))) 84 | d.forEach(function(d){ d.w = d.w > 2 ? d.w : 0 }) 85 | }) 86 | 87 | 88 | var advanceSlots = 2 89 | 90 | d3.nestBy(teams, function(d){ return d.w + d.wins*10 }) 91 | .sort(d3.descendingKey('key')) 92 | .forEach(function(d){ 93 | if (d.length <= advanceSlots){ 94 | d.forEach(function(d){ d.advance = 't'}) 95 | if(advanceSlots == 2){ 96 | if(d.length == 2){ //check for tied first place 97 | against = matches.filter(match => {return match.allTeams == d[0].name + '-' + d[1].name 98 | || match.allTeams == d[1].name + '-' + d[0].name}) 99 | if(against[0].actualWinner == against[1].actualWinner){ 100 | d.forEach(function(d){ if (d.name == against[0].actualWinner) d.advance = 'u'}) 101 | } 102 | } 103 | else 104 | d.forEach(function(d){ d.advance = 'u'}) 105 | } 106 | } else if (advanceSlots > 0){ 107 | d.forEach(function(d){ d.advance = 'm'}) 108 | } else{ 109 | d.forEach(function(d){ d.advance = 'f' }) 110 | if(d.length == 2){ //check for tied last place 111 | against = matches.filter(match => {return match.allTeams == d[0].name + '-' + d[1].name 112 | || match.allTeams == d[1].name + '-' + d[0].name}) 113 | if(against[0].actualWinner == against[1].actualWinner){ 114 | d.forEach(function(d){ if (d.name != against[0].actualWinner) d.advance = 'e'}) 115 | } 116 | } 117 | if(advanceSlots == - 1){ 118 | d[0].advance = 'e' 119 | } 120 | } 121 | advanceSlots -= d.length 122 | }) 123 | 124 | function addMatchWins(d){ nameToTeam[d['t' + d.winner]].w++ } 125 | 126 | 127 | return teams 128 | } 129 | 130 | function drawGroup(gMatches){ 131 | var sel = d3.select('#group-' + gMatches.key.toLowerCase()).html('') 132 | 133 | var complete = gMatches.filter(d => d.complete) 134 | var incomplete = gMatches.filter(function(d){ return !d.complete }) 135 | scenarios = d3.range(64).map(function(i){ 136 | incomplete.forEach(function(d, j){ 137 | d.winner = (i >> j) % 2 ? 1 : 2 138 | d.wName = d['t' + d.winner] 139 | }) 140 | 141 | return { 142 | str: incomplete.map(ƒ('winner')).join(''), 143 | teams: scoreMatches(gMatches), 144 | incomplete: JSON.parse(JSON.stringify(incomplete))} 145 | }) 146 | 147 | var teams = d3.nestBy(gMatches, ƒ('t1')).map(function(d){ 148 | return {name: d.key, w: 0, actualWins: teams2wins[d.key]} 149 | }).sort(d3.descendingKey('actualWins')) 150 | 151 | sel.appendMany('div.team', teams) 152 | .each(function(d){ drawResults(d3.select(this), scenarios, d.name, complete, incomplete) }) 153 | 154 | incomplete.forEach(function(d){ d.clicked = (+d.actualWinner || 3) - 1 }) 155 | var gameSel = sel.append('div.matches') 156 | .st({marginTop: 50}) 157 | .appendMany('div.game', incomplete) 158 | .on('click', function(d){ 159 | d.clicked = (d.clicked + 1) % 3 160 | 161 | d3.select(this).selectAll('.teamabv') 162 | .classed('won', function(e, i){ return i + 1 == d.clicked }) 163 | d3.select(this).classed('active', d.clicked) 164 | 165 | var str = incomplete.map(ƒ('clicked')).join('') 166 | sel.selectAll('circle.scenario').classed('hidden', function(d){ 167 | return d.incomplete.some(function(d, i){ 168 | return str[i] != '0' && str[i] != d.winner 169 | }) 170 | }) 171 | }) 172 | gameSel.append('span.teamabv').text(ƒ('t1')) 173 | gameSel.append('span').text(' v. ') 174 | gameSel.append('span.teamabv').text(ƒ('t2')) 175 | gameSel.each(function(d){ d3.select(this).on('click').call(this, d) }) 176 | } 177 | 178 | 179 | function drawResults(sel, scenarios, name, complete, incomplete){ 180 | 181 | scenarios.forEach(function(d){ 182 | d.team = d.teams.filter(function(d){ return d.name == name })[0] 183 | d.wins = d.team.wins 184 | 185 | d.playedIn = d.incomplete.filter(function(d){ 186 | d.currentWon = name == d.wName 187 | return name == d.t1 || name == d.t2 }) 188 | d.recordStr = d.playedIn.map(function(d){ return +d.currentWon }).join('') 189 | }) 190 | 191 | var against = [] 192 | scenarios[0].playedIn.forEach(function(d){ 193 | var otherTeam = name == d.t1 ? d.t2 : d.t1 194 | against.push(otherTeam) 195 | }) 196 | 197 | var completeIn = complete.filter(function(d){ 198 | d.currentWon = name == d.wName 199 | d.otherTeam = name == d.t1 ? d.t2 : d.t1 200 | return name == d.t1 || name == d.t2 }) 201 | 202 | var pBeat = completeIn.filter(ƒ('currentWon')) 203 | var pLost = completeIn.filter(function(d){ return !d.currentWon }) 204 | 205 | var pStr = 'Lost to ' 206 | pStr += pLost.map(ƒ('otherTeam')).join(' and ') 207 | 208 | if (pBeat.length){ 209 | pStr += ' // Beat ' + pBeat.map(ƒ('otherTeam')).join(' and ') 210 | } else{ 211 | pStr = pStr.replace(' and ', ', ') 212 | } 213 | if (!pLost.length) pStr = pStr.replace('Lost to //', '').replace(' and ', ', ') 214 | // pStr += ' previously' 215 | 216 | var byWins = d3.nestBy(scenarios, ƒ('wins')) 217 | byWins.forEach(function(d, i){ 218 | d.sort(d3.descendingKey(ƒ('team', 'advance'))) 219 | d.byRecordStr = _.sortBy(d3.nestBy(d, ƒ('recordStr')), 'key') 220 | if (i == 1) d.byRecordStr.reverse() 221 | }) 222 | 223 | var width = 315, height = 300 224 | var svg = sel.append('svg').at({width, height}).st({margin: 20}) 225 | .append('g').translate([0, 100]) 226 | var gSel = d3.select(sel.node().parentNode) 227 | 228 | var swoopySel = svg.append('g.annotations') 229 | 230 | svg.append('text').text(name) 231 | .translate([10*3.5 + 100, -60]).at({textAnchor: 'middle', fontSize: 20}) 232 | 233 | svg.append('text').text(pStr) 234 | .translate([10*3.5 + 100, -45]).at({textAnchor: 'middle', fontSize: 12, fill: '#888'}) 235 | 236 | 237 | var winsSel = svg.appendMany('g', byWins.sort(d3.descendingKey('key'))) 238 | .translate(function(d, i){ return [0, i*80 + (i == 3 ? -15*2 : i > 0 ? -8 : 0)] }) 239 | 240 | winsSel.append('text') 241 | .text(function(d, i){ return i == 1 ? 'Only Lose To...' : i == 2 ? 'Only Beat...' : '' }) 242 | .at({textAnchor: 'middle', x: 10*3.5 + 100, y: -30, fill: '#888', fontSize: 12}) 243 | 244 | var recordSel = winsSel.appendMany('g', ƒ('byRecordStr')) 245 | .translate(function(d, i){ return [d.key == '000' || d.key == '111' ? 100 : i*100, 0] }) 246 | 247 | recordSel.append('text') 248 | .text(function(d){ 249 | var s 250 | if (d.key == '111') s = 'Win Next Three' 251 | if (d.key == '000') s = 'Lose Next Three' 252 | if (d.key == '001') s = against[2] 253 | if (d.key == '010') s = against[1] 254 | if (d.key == '100') s = against[0] 255 | if (d.key == '011') s = against[0] 256 | if (d.key == '101') s = against[1] 257 | if (d.key == '110') s = against[2] 258 | return s 259 | }) 260 | .at({textAnchor: 'middle', x: 10*3.5, y: -10}) 261 | 262 | recordSel.appendMany('circle.scenario', ƒ()) 263 | .at({r: 5, fill: ƒ('team', color[colorChoice]), cx: function(d, i){return i*10} }) 264 | .call(d3.attachTooltip) 265 | .on('mouseout', function(){gSel.selectAll('circle.scenario').classed('active', false).at('r', 5) }) 266 | .on('mouseover', function(d){ 267 | gSel.selectAll('circle.scenario') 268 | .classed('active', 0) 269 | .attr('r', 5) 270 | .filter(function(e){ return d.str == e.str }) 271 | .classed('active', 1) 272 | .attr('r', 8) 273 | .raise() 274 | 275 | var tt = d3.select('.tooltip').html('') 276 | var gameSel = tt.appendMany('div.game', incomplete) 277 | gameSel.append('span').text(ƒ('t1')).classed('won', function(e, i){ return d.str[i] == 1 }) 278 | gameSel.append('span').text(' v. ') 279 | gameSel.append('span').text(ƒ('t2')).classed('won', function(e, i){ return d.str[i] == 2 }) 280 | 281 | var byAdvanceSel = tt.appendMany('div.advance', d3.nestBy(d.teams, ƒ('advance')).sort(d3.descendingKey('key'))) 282 | .text(function(d){ 283 | return d.map(ƒ('name')).join(' and ') + {u:' first seed',t: ' advance', m: ' tie', f: (d.length > 1 ? ' are' : ' is') + ' eliminated', e:' last place'}[d.key] 284 | }) 285 | }) 286 | 287 | .on('click', function(d){ 288 | current = [] 289 | blank = true 290 | d3.selectAll('.matches > .game').each(function(e, i){ 291 | if (e.group == d.incomplete[0].group){ 292 | current.push(e.clicked) 293 | if (e.clicked != 0) blank = false 294 | } 295 | }) 296 | reset = !d3.select(this).classed('hidden') && !blank 297 | d3.selectAll('.matches > .game').each(function(e, i){ 298 | if (e.group == d.incomplete[0].group){ 299 | if(reset) e.clicked = -1 300 | else if(blank) 301 | e.clicked = parseInt(d.str[i % 6]) - 1 302 | else if(parseInt(d.str[i % 6]) != current[i % 6]) 303 | e.clicked = -1 304 | else e.clicked-- 305 | d3.select(this).dispatch("click") 306 | } 307 | }) 308 | }) 309 | 310 | var swoopy = d3.swoopyDrag() 311 | .draggable(0) 312 | .x(function(){ return 0 }) 313 | .y(function(){ return 0 }) 314 | .annotations(annotations.filter(function(d){ return d.team == name })) 315 | 316 | swoopySel.call(swoopy) 317 | swoopySel.selectAll('path').attr('marker-end', 'url(#arrow)') 318 | swoopySel.selectAll('text') 319 | .each(function(d){ 320 | d3.select(this) 321 | .text('') //clear existing text 322 | .tspans(d3.wordwrap(d.text, d.lw || 5)) //wrap after 20 char 323 | }) 324 | 325 | } 326 | 327 | 328 | color = [ function color(d){ return {u:'#4CAF50',t: '#4CAF50', m: '#FF9800', f: '#f73434', e: '#f73434'}[d.advance] }, 329 | function color(d){ return {u:'#2c7bb6',t: '#2c7bb6', m: '#dfaf90', f: '#f73434', e: '#f73434'}[d.advance] }, 330 | function color(d){ return {u:'#36e03d',t: '#4CAF50', m: '#FF9800', f: '#f73434', e: '#b80f02'}[d.advance] }, 331 | function color(d){ return {u:'#5aa4db',t: '#2c7bb6', m: '#dfaf90', f: '#f73434', e: '#b80f02'}[d.advance] }] 332 | 333 | d3.select('html').selectAppend('div.tooltip').classed('tooltip-hidden', 1) 334 | 335 | d3.select('html').selectAppend('svg.marker') 336 | .append('marker') 337 | .attr('id', 'arrow') 338 | .attr('viewBox', '-10 -10 20 20') 339 | .attr('markerWidth', 20) 340 | .attr('markerHeight', 20) 341 | .attr('orient', 'auto') 342 | .append('path') 343 | .attr('d', 'M-6.75,-6.75 L 0,0 L -6.75,6.75') 344 | --------------------------------------------------------------------------------