console.clear() var isMobile = innerWidth < 1000 d3.select('body').classed('is-mobile', isMobile) var colors = ['#FDE100', '#EE2737' ] var colors = ['#FDE100', '#8e068e' ] // var colors = ['#2979FF', '#FF6D00'] // var colors = ['#2979FF', '#FDD835'] // var colors = ['#f1a340', '#998ec3' ] var color2dark = { '#FDE100': d3.color('#FDE100').darker(.2), '#8e068e': d3.color('#8e068e').darker(2), } var colorScale = d3.interpolate(colors[0], colors[1]) var s = d3.select('#field-grass').node().offsetWidth/120 var width = 120*s var height = Math.floor(75*s) var cs = 20 var cells = d3.cross( d3.range(0, width + cs, cs), d3.range(0, height + cs, cs)) globalPlayers = decoratePlayers(players0) globalPlayersH = decoratePlayers(playersleaklow) function decoratePlayers(rawPlayers){ var players = rawPlayers.map(d => d.map(d => d*s)) players.forEach((d, i) => { d.color = i < 11 ? colors[0] : colors[1] d.isRed = i < 11 ? 1 : 0 d.i = i }) players.renderFns = [] players.renderAll = () => players.renderFns.forEach(d => d()) return players } var playerOptions0 = [players1, players2, players0] var playerOptions1 = [playersleaklow, playersleakhigh] // addPlayAnimation(globalPlayers, '#field-grass', playerOptions0, 'mouseenter') addPlayAnimation(globalPlayers, '#player-button', playerOptions0) addPlayAnimation(globalPlayersH, '#high-button', playerOptions1, 'click', true) function addPlayAnimation(players, selStr, playerOptions, eventStr='click', loop=false){ if (loop) { window.loopInterval = d3.interval(playAnimation, 2500) } if (selStr) { d3.selectAll(selStr).on(eventStr, function() { if (loop) window.loopInterval.stop() // stop looping if the higher-or-lower button is pressed playAnimation() }) } var curPlayerIndex = 0 function playAnimation(){ curPlayerIndex++ curPlayerIndex = curPlayerIndex % playerOptions.length var nextPlayers = playerOptions[curPlayerIndex] .map(d => d.map(d => d*s)) var interpolates = players .map((d, i) => d3.interpolate(d, nextPlayers[i])) var dur = 1000 if (playerOptions.animationTimer) playerOptions.animationTimer.stop() playerOptions.animationTimer = d3.timer(time => { var t = d3.clamp(0, time/dur, 1) interpolates.forEach((interpolate, i) => { var [x, y] = interpolate(t) players[i][0] = x players[i][1] = y }) players.renderAll(t) if (t == 1) playerOptions.animationTimer.stop() }) } } function stopAnimations(){ if (playerOptions0.animationTimer) playerOptions0.animationTimer.stop() if (playerOptions1.animationTimer) playerOptions1.animationTimer.stop() } function initField(name){ var marginBottom = 30 var marginTop = 35 var sel = d3.select('#field-' + name).html('').classed('field', true) .st({marginBottom: marginBottom, marginTop: marginTop}) window.c = d3.conventions({ sel, margin: {top: 0, left: 0, right: 0, bottom: 0}, width, height, layers: 'dcs' }) var [divSel, ctx, svg] = c.layers c.svg = c.svg.append('g').translate([.5, .5]) var isRegression = name.includes('regression') var isVisiblePoints = name != 'playerless' var pointName = isRegression || name == 'scatter' ? ' People' : ' Players' var buttonSel = sel.append('div.button') .st({top: pointName == ' People' ? 28 : -8, right: -8, position: 'absolute', background: '#fff'}) .text((isVisiblePoints ? 'Hide' : 'Show') + pointName) .on('click', () => { isVisiblePoints = !isVisiblePoints buttonSel.text((isVisiblePoints ? 'Hide' : 'Show') + pointName) playerSel.st({opacity: isVisiblePoints ? 1 : 0}) textSel.st({opacity: isVisiblePoints ? 1 : 0}) }) if (name == 'grass'){ c.svg.append('rect').at({width, height, fill: '#34A853'}) divSel.append('div.pointer').append('div') } var roundNum = d => isNaN(d) ? d : Math.round(d) var chalkSel = c.svg.append('g') chalkSel.append('path.white') .at({d: ['M', Math.round(width/2), 0, 'V', height].map(roundNum).join(' '),}) chalkSel.append('circle.white') .at({r: 10*s}).translate([width/2, height/2]) chalkSel.append('path.white') .at({d: ['M', 0, (75 - 44)/2*s, 'h', 18*s, 'v', 44*s, 'H', 0].map(roundNum).join(' '),}) chalkSel.append('path.white') .at({d: ['M', width, (75 - 44)/2*s, 'h', -18*s, 'v', 44*s, 'H', width].map(roundNum).join(' '),}) var drag = d3.drag() .on('drag', function(d){ stopAnimations() if (name === 'regression-leak') { window.loopInterval.stop() } d[0] = Math.round(Math.max(0, Math.min(width, d3.event.x))) d[1] = Math.round(Math.max(0, Math.min(height, d3.event.y))) players.renderAll() }) .subject(function(d){ return {x: d[0], y: d[1]} }) var players = name == 'regression-leak' ? globalPlayersH : globalPlayers if (isRegression){ var byColor = d3.nestBy(players, d => d.color) var regressionSel = c.svg.appendMany('path', byColor) .at({stroke: d => color2dark[d.key], strokeWidth: 3.5, strokeDasharray: '4 4'}) .each(function(d){ d.sel = d3.select(this) }) } var bgPlayerSel = c.svg.appendMany('circle.player', players) .at({r: 15, fill: d => d.color, opacity: 0}) .translate(d => d) .call(drag) var playerSel = c.svg.appendMany('circle.player', players) .at({r: 5, fill: d => d.color, opacity: isVisiblePoints ? 1 : 0}) .translate(d => d) .call(drag) var textSel = c.svg.appendMany('text.chart-title', name == 'playerless' ? [players[0], players[20]] : [players[0]]) .text(name == 'regression-leak' || name == 'scatter' ? 'New Hire' : name == 'playerless' ? 'Goalie' : '') .st({pointerEvent: 'none'}) .at({dy: '.33em', opacity: isVisiblePoints ? 1 : 0, dx: (d, i) => i ? -8 : 8, textAnchor: (d, i) => i ? 'end' : 'start'}) if (name == 'scatter' || isRegression){ sel.st({marginBottom: marginBottom + 70}) sel.insert('div.axis.chart-title', ':first-child') .html(` Men's and Women's Salaries`) .st({marginBottom: 10, fontSize: 16}) c.x.domain([0, 20]) c.y.domain([40000, 90000]) c.xAxis.ticks(5) c.yAxis.ticks(5).tickFormat(d => { var rv = d3.format(',')(d).replace('9', '$9') if (isMobile){ rv = rv.replace(',000', 'k').replace('40k', '') } return rv }) chalkSel.selectAll('*').remove() chalkSel.appendMany('path.white', c.x.ticks(5)) .at({d: d => ['M', Math.round(c.x(d)), '0 V ', c.height].join(' ')}) chalkSel.appendMany('path.white', c.y.ticks(5)) .at({d: d => ['M 0', Math.round(c.y(d)), 'H', c.width].join(' ')}) d3.drawAxis(c) c.svg.selectAll('.axis').lower() if (isMobile){ c.svg.selectAll('.y text') .translate([35, 10]) .st({fill: name == 'scatter' ? '#000' : ''}) c.svg.selectAll('.x text').filter(d => d == 20).at({textAnchor: 'end'}) c.svg.selectAll('.x text').filter(d => d == 0).at({textAnchor: 'start'}) } c.svg.select('.x').append('text.chart-title') .text('Years at Company →') .translate([c.width/2, 43]) .at({textAnchor: 'middle'}) } render() players.renderFns.push(render) function render(){ renderSVG() if (name != 'grass' && !isRegression)renderCanvas() if (isRegression) renderRegression() } function renderSVG(){ if (playerSel){ playerSel.translate(d => d) bgPlayerSel.translate(d => d) textSel.translate(d => d) } } function renderCanvas(){ cells.forEach(d => { players.forEach(p => { var dx = p[0] - d[0] - cs/2 var dy = p[1] - d[1] - cs/2 // p.dist = Math.sqrt(dx*dx + dy*dy) // p.dist = dx*dx + dy*dy p.dist = Math.pow(dx*dx + dy*dy, 1.5) + .00001 p.weight = 1/p.dist return p.dist }) var sum = d3.sum(players, d => d.isRed*d.weight) var wsum = d3.sum(players, d => d.weight) ctx.fillStyle = colorScale(1 - sum/wsum) ctx.fillRect(d[0], d[1], cs, cs) }) } function renderRegression(){ byColor.forEach(d => { var l = ss.linearRegressionLine(ss.linearRegression(d)) var x0 = 0 var x1 = c.width d.sel.at({d: `M ${x0} ${l(x0)} L ${x1} ${l(x1)}`}) }) } } 'grass prediction playerless scatter regression regression-leak' .split(' ') .forEach(initField)