|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 == 131) collegeGPA = 3.9 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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 (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(){ |
|
|
|
var isDrag = 0 |
|
if (!isDrag) annotations.forEach(d => d.text = d.html ? '' : d.text) |
|
if (isDrag){ |
|
d3.select('#sections').st({pointerEvents: 'none'}) |
|
} |
|
|
|
|
|
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('') |
|
.tspans(d3.wordwrap(d.text, d.width || 20), 13) |
|
}) |
|
})() |
|
|
|
|
|
|
|
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: '#eee', |
|
stroke: '#aaa', |
|
strokeWidth: .5, |
|
opacity: 0, |
|
|
|
}) |
|
|
|
|
|
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' |
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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}) |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|