Spaces:
Running
Running
| /* Copyright 2020 Google LLC. All Rights Reserved. | |
| Licensed under the Apache License, Version 2.0 (the "License"); | |
| you may not use this file except in compliance with the License. | |
| You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| ==============================================================================*/ | |
| var ttSel = d3.select('body').selectAppend('div.tooltip.tooltip-hidden') | |
| var colors = { | |
| m: '#7DDAD3', | |
| f: '#9B86EF', | |
| h: '#F0BD80', | |
| l: '#FF777B', | |
| grey: '#ccc', | |
| } | |
| var totalWidth = width = d3.select('#graph').node().offsetWidth | |
| var r = 40 | |
| var sel = d3.select('#graph').html('') | |
| .append('div') | |
| var extraWidth = d3.clamp(500, innerHeight - 150, innerWidth - 500) | |
| var scale = extraWidth/500 | |
| scale = 1 | |
| sel.st({transform: `scale(${scale})`, transformOrigin: '0% 0%'}) | |
| var c = d3.conventions({ | |
| sel, | |
| totalWidth, | |
| totalHeight: totalWidth, | |
| margin: {left: 25, right: 7}, | |
| layers: 'sd', | |
| }) | |
| var divSel = c.layers[1] | |
| c.x.domain([1, 4]).clamp(true).interpolate(d3.interpolateRound) | |
| c.y.domain([1, 4]).clamp(true).interpolate(d3.interpolateRound) | |
| c.xAxis.ticks(3).tickFormat(d3.format('.1f')) | |
| c.yAxis.ticks(3).tickFormat(d3.format('.1f')) | |
| d3.drawAxis(c) | |
| var axis2Sel= c.svg.append('g.axis').append('line') | |
| .translate(Math.round(c.y(2)) + .5, 1) | |
| .at({x2: c.width, stroke: '#000', opacity: 0}) | |
| var meanGPADiff = .6 | |
| var seed = new Math.seedrandom('hii') | |
| var students = d3.range(150).map((d, index) => { | |
| var collegeGPA = d3.randomUniform.source(seed)(1, 4)() | |
| // if (index == 93) collegeGPA = 2.05 | |
| // if (index == 87) collegeGPA = 2.15 | |
| // if (index == 32) collegeGPA = 2.25 | |
| if (index == 131) collegeGPA = 3.9 | |
| // var hsGPA = collegeGPA*d3.randomNormal(1, .4)() | |
| var hsGPA = collegeGPA + d3.randomNormal.source(seed)(meanGPADiff, .8)() | |
| var hsGPAadjusted = hsGPA - meanGPADiff | |
| var rand = d3.randomUniform.source(seed)(0, 1) | |
| var isMale = rand() < .5 | |
| var name = names[isMale ? 'm' : 'f'][Math.floor(d/2)] | |
| var lastName = names.last[d] | |
| var maleOffset = rand()*(isMale ? 1 : -1)*.6 | |
| // if (index == 47) name = 'Mia' | |
| // if (index == 82) name = 'Mason' | |
| var compGPA0 = lerp(hsGPAadjusted, collegeGPA, rand()*.7) + maleOffset | |
| var compGPA1 = lerp(compGPA0, collegeGPA + maleOffset, rand()*1.1) | |
| var compGPA2 = compGPA1 + rand()/4 - 1/4/2 | |
| // var compGPA0 = collegeGPA + d3.randomNormal.source(seed)(0, .5)() | |
| // var compGPA1 = collegeGPA + d3.randomNormal.source(seed)(0, .3)() | |
| if (index == 69){ | |
| compGPA1 = 2.0 | |
| } | |
| if (index == 37){ | |
| compGPA1 = 2.0 | |
| } | |
| var isLowIncome = rand() < .5 | |
| var inteviewGPA = collegeGPA + d3.randomNormal.source(seed)(0, .15)() | |
| var inteviewGPAbias = inteviewGPA + rand()*(isLowIncome ? -1 : 1)*.5 | |
| // if (index == 115) name = 'Mason' | |
| // if (index == 32) name = 'Mia' | |
| if (name == 'Camila') name = 'Mia' | |
| return {name, index, lastName, collegeGPA, hsGPA, hsGPAadjusted, compGPA0, compGPA1, compGPA2, isMale, isLowIncome, inteviewGPA, inteviewGPAbias} | |
| }) | |
| students = _.sortBy(students, d => d.collegeGPA) | |
| students = students.filter(d => { | |
| return d3.entries(d).every(({key, value}) => { | |
| if (!key.includes('GPA')) return true | |
| return 1 < value && value < 4.0 | |
| }) | |
| }) | |
| c.svg.append('path') | |
| .at({ | |
| d: ['M', 0, c.height, 'L', c.width, 0].join(' '), | |
| stroke: '#ccc', | |
| strokeWidth: 2, | |
| strokeDasharray: '4 2' | |
| }) | |
| !(function(){ | |
| // return window.annotationSel = d3.select(null) | |
| var isDrag = 0 | |
| if (!isDrag) annotations.forEach(d => d.text = d.html ? '' : d.text) | |
| if (isDrag){ | |
| d3.select('#sections').st({pointerEvents: 'none'}) | |
| } | |
| // copy('window.annotations = ' + JSON.stringify(annotations, null, 2)) | |
| var swoopy = d3.swoopyDrag() | |
| .x(d => c.x(d.x)) | |
| .y(d => c.y(d.y)) | |
| .draggable(isDrag) | |
| .annotations(annotations) | |
| .on('drag', d => { | |
| }) | |
| var htmlAnnoSel = divSel.appendMany('div.annotation', annotations.filter(d => d.html)) | |
| .translate(d => [c.x(d.x), c.y(d.y)]).st({position: 'absolute', opacity: 0}) | |
| .append('div') | |
| .translate(d => d.textOffset) | |
| .html(d => d.html) | |
| .st({width: 150}) | |
| var swoopySel = c.svg.append('g.annotations').call(swoopy) | |
| c.svg.append('marker') | |
| .attr('id', 'arrow') | |
| .attr('viewBox', '-10 -10 20 20') | |
| .attr('markerWidth', 20) | |
| .attr('markerHeight', 20) | |
| .attr('orient', 'auto') | |
| .append('path') | |
| .attr('d', 'M-6.75,-6.75 L 0,0 L -6.75,6.75') | |
| swoopySel.selectAll('path') | |
| .attr('marker-end', 'url(#arrow)') | |
| .st({'opacity': d => d.path == 'M 0 0' ? 0 : 1}) | |
| window.annotationSel = swoopySel.selectAll('g') | |
| .st({fontSize: 12, opacity: d => d.slide == 0 ? 1 : 0}) | |
| window.annotationSel = d3.selectAll('g.annotations g, div.annotation') | |
| swoopySel.selectAll('text') | |
| .each(function(d){ | |
| d3.select(this) | |
| .text('') //clear existing text | |
| .tspans(d3.wordwrap(d.text, d.width || 20), 13) //wrap after 20 char | |
| }) | |
| })() | |
| students = _.sortBy(students, d => d.collegeGPA) | |
| var lineSel = c.svg.appendMany('path', students) | |
| .translate(d => [c.x(d.hsGPA), c.y(d.collegeGPA)]) | |
| .at({ | |
| // fill: d => d.hsGPA > d.collegeGPA ? 'blue' : 'orange', | |
| fill: '#eee', | |
| stroke: '#aaa', | |
| strokeWidth: .5, | |
| opacity: 0, | |
| // strokeWidth: 1/scale, | |
| }) | |
| var circleSel = c.svg.appendMany('g', students) | |
| .translate(d => [c.x(d.collegeGPA), c.y(d.hsGPA)]) | |
| .call(d3.attachTooltip) | |
| .on('mouseover', d => { | |
| var html = '' | |
| html += `<div><b>${d.name} ${d.lastName}</b></div>` | |
| if (curSlide.circleFill == 'gender'){ | |
| html += `<span style='background: ${colors[d.isMale ? 'm' : 'f']}'>${d.isMale ? 'Male' : 'Female'}</span>` | |
| } | |
| if (curSlide.circleFill == 'income'){ | |
| html += `<span style='background: ${colors[d.isLowIncome ? 'l' : 'h']}'>${d.isLowIncome ? 'Low Income' : 'High Income'}</span>` | |
| } | |
| html += ` | |
| <div><b>${d3.format('.2f')(d[curSlide.yKey]).slice(0, 4)}</b> ${curSlide.index ? 'Predicted' : 'High School'} GPA</div> | |
| <div><b>${d3.format('.2f')(d.collegeGPA).slice(0, 4)}</b> College GPA</div>` | |
| ttSel.html(html) | |
| }) | |
| var innerCircleSel = circleSel.append('circle') | |
| .at({ | |
| r: 5, | |
| fill: '#eee', | |
| stroke: '#aaa' | |
| }) | |
| // var textSel = circleSel.append('text').text(d => d.isMale ? 'M' : 'F') | |
| // .at({textAnchor: 'middle', dy: '.33em', fontSize: 8, fill: '#eee'}) | |
| // var textSel2 = circleSel.append('text').text(d => d.isLowIncome ? 'L' : 'H') | |
| // .at({textAnchor: 'middle', dy: '.33em', fontSize: 8, opacity: 0}) | |
| c.svg.select('.y').selectAll('line').filter(d => d == 4) | |
| .remove() | |
| c.svg.select('.y').selectAll('text').filter(d => d == 4) | |
| .select(function() { | |
| return this.parentNode.insertBefore(this.cloneNode(1), this.nextSibling); | |
| }) | |
| .text('Actual College GPA') | |
| .at({x: c.width/2, y: c.height + 35, textAnchor: 'middle', fontWeight: 800}) | |
| var yLabelSel = divSel.st({pointerEvents: 'none'}).append('div.axis') | |
| .html('<b>High School GPA</b>') | |
| .translate([0, -9]) | |
| .st({textAlign: 'left', maxWidth: 260}) | |
| // c.svg.append('text').text('Actual College GPA').st({fontWeight: 800}) | |
| var longLabel = 'high school GPA, essay, clubs, zip code, teacher recommendations, sports, AP scores, demonstrated interest, gender, SAT scores, interviews, portfolio, race, work experience' | |
| var slides = [ | |
| { | |
| yKey: 'hsGPA', | |
| isLineVisible: 0, | |
| yLabel: '<b>High School GPA</b>', | |
| circleFill: 'grey', | |
| circleFillDelay: d => 0, | |
| }, | |
| { | |
| yKey: 'hsGPA', | |
| isLineVisible: true, | |
| yLabel: '<b>High School GPA</b>' | |
| }, | |
| { | |
| yKey: 'hsGPAadjusted', | |
| yLabel: 'high school GPA' | |
| }, | |
| { | |
| yKey: 'compGPA0', | |
| yLabel: 'high school GPA, essay, clubs, zip code'.replace('essay', '<span class="highlight blue">essay') + '</span>' | |
| }, | |
| { | |
| yKey: 'compGPA1', | |
| yLabel: longLabel.replace('teacher', '<span class="highlight blue">teacher') + '</span>', | |
| circleFill: 'grey', | |
| circleFillDelay: d => 0, | |
| textFill: '#eee', | |
| }, | |
| { | |
| yKey: 'compGPA1', | |
| yLabel: longLabel, | |
| circleFill: 'gender', | |
| circleFillDelay: (d, i) => i*20 + (d.isMale ? 0 : 2000), | |
| textFill: '#000', | |
| }, | |
| { | |
| name: 'proxyHighlight', | |
| yKey: 'compGPA2', | |
| yLabel: longLabel, | |
| circleFill: 'gender', | |
| circleFillDelay: d => 0, | |
| textFill: '#000', | |
| }, | |
| { | |
| textFill: '#eee', | |
| yLabel: 'Alumni interview', | |
| yKey: 'inteviewGPAbias', | |
| circleFill: 'grey', | |
| text2Opacity: 0, | |
| }, | |
| { | |
| textFill: '#eee', | |
| yLabel: 'Alumni interview', | |
| yKey: 'inteviewGPAbias', | |
| circleFill: 'income', | |
| circleFillDelay: (d, i) => i*20 + (!d.isLowIncome ? 2000 : 0), | |
| text2Opacity: 1, | |
| }, | |
| { | |
| textFill: '#eee', | |
| yLabel: 'Alumni interview, household income'.replace('household', '<span class="highlight blue">household') + '</span>', | |
| yKey: 'inteviewGPA', | |
| text2Opacity: 1, | |
| }, | |
| ] | |
| slides.forEach(d => { | |
| if (d.name == 'proxyHighlight'){ | |
| var proxies = 'clubs, interviews, portfolio, sports'.split(', ') | |
| d.yLabel = d.yLabel | |
| .split(', ') | |
| .map(d => { | |
| if (d == 'gender') return `<span class='strikethrough'>gender</span>` | |
| if (!proxies.includes(d)) return d | |
| return `<span class='highlight yellow'>${d}</span>` | |
| }) | |
| .join(', ') | |
| } | |
| if (d.yLabel[0] != '<') d.yLabel = '<b>Predicted College GPA</b> using ' + d.yLabel.replace('School', 'school') | |
| }) | |
| var keys = [] | |
| slides.forEach(d => keys = keys.concat(d3.keys(d))) | |
| _.uniq(keys).forEach(str => { | |
| var prev = null | |
| slides.forEach(d => { | |
| if (typeof(d[str]) === 'undefined'){ | |
| d[str] = prev | |
| } | |
| prev = d[str] | |
| }) | |
| }) | |
| slides.forEach((d, i) => { | |
| d.circleFillFn = { | |
| grey: d => '#eee', | |
| gender: d => d.isMale ? colors.m : colors.f, | |
| income: d => d.isLowIncome ? colors.l : colors.h, | |
| }[d.circleFill] | |
| d.index = i | |
| }) | |
| var gs = d3.graphScroll() | |
| .container(d3.select('.container-1')) | |
| .graph(d3.selectAll('container-1 #graph')) | |
| .eventId('uniqueId1') | |
| .sections(d3.selectAll('.container-1 #sections > div')) | |
| .offset(innerWidth < 900 ? 300 : 520) | |
| .on('active', updateSlide) | |
| var prevSlide = -1 | |
| function updateSlide(i){ | |
| var slide = slides[i] | |
| if (!slide) return | |
| curSlide = slide | |
| var {yKey} = slide | |
| lineSel.transition('yKey').duration(500) | |
| .at({ | |
| d: d => [ | |
| 'M 5 0', | |
| 'C 0 0', | |
| 0, c.y(d['collegeGPA']) - c.y(d[yKey]), | |
| 0, c.y(d['collegeGPA']) - c.y(d[yKey]), | |
| 'S 0 0 -5.5 0' | |
| ].join(' ') | |
| }) | |
| .translate(d => [c.x(d.collegeGPA), c.y(d[yKey])]) | |
| circleSel.transition('yKey').duration(500) | |
| .translate(d => [c.x(d.collegeGPA), c.y(d[yKey])]) | |
| innerCircleSel.transition('colorFill').duration(30) | |
| .delay(slide.circleFillDelay) | |
| .at({ | |
| fill: slide.circleFillFn, | |
| stroke: d => d3.color(slide.circleFillFn(d)).darker(1.5) | |
| }) | |
| axis2Sel.transition() | |
| .st({opacity: i == 5 ? 1 : 0}) | |
| lineSel.transition('opacity').duration(500) | |
| .st({ | |
| opacity: slide.isLineVisible ? 1 : 0 | |
| }) | |
| if (slide.yLabel) yLabelSel.html(slide.yLabel) | |
| annotationSel.transition() | |
| .st({opacity: d => i == d.slide ? 1 : 0}) | |
| prevSlide = i | |
| } | |
| slide = slides[0] | |
| d3.selectAll('.circle').each(function(){ | |
| var d = d3.select(this).attr('class').split(' ')[0] | |
| d3.select(this) | |
| .st({ | |
| backgroundColor: d3.color(colors[d]), | |
| borderColor: d3.color(colors[d]).darker(1.5), | |
| }) | |
| }) | |
| function lerp(a, b, t){ return a + t*(b - a) } | |
| c.svg.selectAll('g.annotations').raise() | |
| d3.selectAll('#sections img').attr('aria-hidden', true) | |