Built with blockbuilder.org
forked from enjalot's block: interviewing.io: mean lines
forked from enjalot's block: interviewing.io: mean vs stddev
Built with blockbuilder.org
forked from enjalot's block: interviewing.io: mean lines
forked from enjalot's block: interviewing.io: mean vs stddev
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> | |
<style> | |
/* OPTIONAL STYLES */ | |
body { | |
margin:0;position:fixed;top:0;right:0;bottom:0;left:0; | |
font-family: Helvetica; | |
} /* not necessary in blog, layout as desired */ | |
#chart { | |
margin-top: 10px; | |
} | |
#chart svg { width:100%; height: 100% } | |
.title { | |
text-align: center; | |
width: 100%; | |
} | |
#explanation { | |
float:left; /* not necessary in blog, layout as desired */ | |
padding: 10px 0px; | |
max-width: 380px; | |
} | |
.tick circle { | |
fill-opacity: 0.4; | |
} | |
/* THIS IS USED TO CALCULATE WIDTH OF CHART */ | |
#chart-container { | |
width: 50%; | |
height: 500px; | |
float:left; | |
} | |
/* NECESSARY STYLES */ | |
span.highlighter { | |
border-bottom: 1px dotted steelblue; | |
} | |
g.cell { | |
cursor: pointer; | |
} | |
circle.node { | |
pointer-events: none; | |
} | |
line.link { | |
stroke: #a0a0a0; | |
stroke-opacity: 0.2; | |
stroke-width: 1; | |
stroke-dasharray: 10 1; | |
pointer-events: none; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: black; | |
shape-rendering: crispEdges; | |
} | |
.axis text { | |
font-family: sans-serif; | |
} | |
.yaxis text { | |
font-size: 9pt; | |
} | |
.xaxis text { | |
font-size: 18px; | |
} | |
.label { | |
font-family: Helvetica; | |
text-anchor: middle; | |
font-size: 14px; | |
fill: #444; | |
} | |
rect.range { | |
fill: #eee; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="chart-container"> | |
<h3 class="title">Standard Deviation vs. Mean of Interviewee Performance (n=67)</h3> | |
<div id="chart"></div> | |
</div> | |
<div id="explanation"> | |
<!-- | |
//available functions | |
clearHighlight(); | |
highlightAll(); | |
highlightLowDeviation(); | |
highlightHighDeviation(); | |
highlightAtLeastOneScore(1); | |
// can also be called with 2 scores like | |
highlightAtLeastOneScore(2, 4); | |
highlightScoreRange(2.5, 3.5); | |
--> | |
You can see everyone who has a <span class="highlighter" onmouseover="highlightHighDeviation();" onmouseout="clearHighlight();">high deviation</span> | |
<br> | |
<br> | |
low variation: <span class="highlighter" onmouseover="highlightLowDeviation()" onmouseout="clearHighlight();">23% (16/67) of interviewees</span> do score consistently across interviews and companies. | |
<br> | |
<br> | |
You can see everyone who scored <span class="highlighter" onmouseover="highlightAtLeastOneScore(1);" onmouseout="clearHighlight();">at least one <span class="score">1</span></span> also scored <span class="score">3</span> or better on other interviews. Good people bomb too. | |
<br> | |
<br> | |
Many people who scored <span class="highlighter" onmouseover="highlightAtLeastOneScore(4,2);" onmouseout="clearHighlight();">at least one <span class="score">4</span></span> also scored at least one <span class="score">2</span>. | |
<br> | |
<br> | |
If we look at <span class="highlighter" onmouseover="highlightScoreRange(3.3,4);" onmouseout="clearHighlight();">high performers</span>, defined as people with a mean score of <span class="score">3.3</span> or higher we still see a fair amount of variation. | |
<br> | |
<br> | |
Things get really murky when we consider <span class="highlighter" onmouseover="highlightScoreRange(2.6, 3.3);" onmouseout="clearHighlight();">"average" performers</span>, defined as people with a mean score between <span class="score">2.6</span> and <span class="score">3.3</span>. | |
<br> | |
<br> | |
Go ahead and <span class="highlighter" onmouseover="highlightAll();" onmouseout="clearHighlight();">expand everybody!</span> | |
<br><br> | |
In the chart to the left every | |
<svg width="10" height="10" viewBox="0 0 8 8"> | |
<path d="M4 0c-1.1 0-2 1.12-2 2.5s.9 2.5 2 2.5 2-1.12 2-2.5-.9-2.5-2-2.5zm-2.09 5c-1.06.05-1.91.92-1.91 2v1h8v-1c0-1.08-.84-1.95-1.91-2-.54.61-1.28 1-2.09 1-.81 0-1.55-.39-2.09-1z" /> | |
</svg> | |
represents a person interviewing.io who has done 2 or more technical interviews. When you hover over a | |
<svg width="10" height="10" viewBox="0 0 8 8"> | |
<path d="M4 0c-1.1 0-2 1.12-2 2.5s.9 2.5 2 2.5 2-1.12 2-2.5-.9-2.5-2-2.5zm-2.09 5c-1.06.05-1.91.92-1.91 2v1h8v-1c0-1.08-.84-1.95-1.91-2-.54.61-1.28 1-2.09 1-.81 0-1.55-.39-2.09-1z" /> | |
</svg> | |
all of that person's individual interview scores are represented with a | |
<svg width=10 height=10> | |
<circle r=4 cx=5 cy=5></circle> | |
</svg>. | |
People are colored by their mean score, with the color scale given by | |
<span class="score">1</span>, | |
<span class="score">2</span>, | |
<span class="score">3</span>, | |
<span class="score">4</span>. | |
</div> | |
<script> | |
var chartWidth = 500; | |
var chartHeight = 500; | |
var margin = { | |
top: 40, right: 60, bottom: 100, left: 90 | |
} | |
var squareWidth = 15; | |
var squareHeight = 15; | |
var scale = 1.5; | |
// hover ranges | |
var ranges = { | |
1: [1, 1.999], | |
2: [2,2.6], | |
3: [2.6, 3.4], | |
4: [3.4, 4] | |
} | |
var colorScale = d3.scale.linear() | |
.domain([1, 2, 3, 4]) | |
.range(["#ff0f5f", "#e63ba8", "#ba48d9", "#267fd3"]) | |
d3.selectAll("span.score") | |
.style("color", function(d) { | |
return colorScale(+this.innerHTML) | |
}) | |
.html(function(d) { | |
var score = +this.innerHTML; | |
return "<svg width=10 height=10><circle r=4 cx=5 cy=5 fill=" + colorScale(score) + "></circle></svg>" + score; | |
}) | |
// https://github.com/iconic/open-iconic/blob/master/svg/person.svg | |
var personIcon = "M4 0c-1.1 0-2 1.12-2 2.5s.9 2.5 2 2.5 2-1.12 2-2.5-.9-2.5-2-2.5zm-2.09 5c-1.06.05-1.91.92-1.91 2v1h8v-1c0-1.08-.84-1.95-1.91-2-.54.61-1.28 1-2.09 1-.81 0-1.55-.39-2.09-1z" | |
var chart = d3.select("#chart") | |
var svg = d3.select("#chart").append("svg") | |
var chartg = svg.append("g"); | |
chartg.append("rect").classed("range", true) | |
var meanLine = chartg.append("line").classed("mean-line", true) | |
var yScale = d3.scale.linear() | |
var xScale = d3.scale.linear() | |
.domain([1, 4]) | |
d3.selection.prototype.moveToFront = function() { | |
return this.each(function(){ | |
this.parentNode.appendChild(this); | |
}); | |
}; | |
d3.json("interviews.json", function(err, interviewees) { | |
//console.log(interviewees) | |
var matches = {}; | |
var meanData = [] ; interviewees.forEach(function(interview, index) { | |
var mean = d3.mean(interview); | |
var stddev = d3.deviation(interview); | |
var points = interview.map(function(score,i) { | |
return { | |
score: score, | |
mean: mean, | |
index: index | |
} | |
}) | |
points.mean = mean; | |
points.stddev = stddev; | |
points.index = index; | |
// we have several interviewees | |
var key = (Math.floor(mean*1000)/1000) + "::" + (Math.floor(stddev*1000)/1000); | |
var match = matches[key]; | |
if(!match) matches[key] = 0; | |
matches[key] += 1; | |
points.offset = matches[key]; | |
meanData.push(points) | |
}) | |
var maxMean = d3.max(meanData, function(d) { return d.mean }); | |
//console.log("maxMean", maxMean) | |
var maxStddev = d3.max(meanData, function(d) { return d.stddev }); | |
//console.log("maxStddev", maxStddev) | |
yScale.domain([0, maxStddev]) | |
var xAxis = d3.svg.axis() | |
.scale(xScale) | |
.orient("bottom") | |
.tickValues([1,2,3,4]) | |
.tickFormat(d3.format(".0n")) | |
var xg = chartg.append("g").classed("axis", true) | |
.classed("xaxis", true) | |
.call(xAxis) | |
xg.selectAll(".tick") | |
.on("mouseover", function(d) { | |
highlightScoreRange(ranges[d][0], ranges[d][1]) | |
}) | |
.on("mouseout", function() { | |
unfade(); | |
clearForce(); | |
}).style({ | |
cursor: "pointer" | |
}) | |
.insert("circle", "line") | |
.attr({ | |
cx: 0, | |
cy: 16, | |
r: 12, | |
fill: function(d) { return colorScale(d)} | |
}) | |
xg.selectAll(".tick").select("text") | |
.attr({ | |
stroke: "#cfe0e7", | |
"paint-order":"stroke", | |
"stroke-width": 2, | |
"stroke-opacity": 1, | |
"stroke-linecap": "butt", | |
"stroke-linejoin": "miter" | |
}) | |
var yAxis = d3.svg.axis() | |
.scale(yScale) | |
.orient("left") | |
var yg = chartg.append("g").classed("axis", true) | |
.classed("yaxis", true) | |
var stdlabel = chartg.append("text").classed("label", true) | |
.text("Standard deviation of interview score") | |
var meanlabel = chartg.append("text").classed("label", true) | |
.text("Mean interview score") | |
function render() { | |
console.log("render", chartWidth, chartHeight) | |
yScale.range([chartHeight - margin.bottom, margin.top]) | |
xScale.range([margin.left, chartWidth - margin.right]) | |
chart.style({ | |
width: chartWidth + "px", | |
height: chartHeight + "px" | |
}) | |
//update axis | |
xg.attr("transform", "translate(" + [0, chartHeight - margin.bottom + 10] + ")") | |
.call(xAxis) | |
yg.attr("transform", "translate(" + [margin.left - 20,-margin.top/2] + ")") | |
.call(yAxis) | |
stdlabel.attr("transform", "translate(" + [30, chartHeight/2 - margin.bottom/2] + ")rotate(-90)") | |
meanlabel.attr("transform", "translate(" + [chartWidth/2 + margin.left/2, chartHeight - margin.bottom/2 + 5] + ")") | |
meanLine | |
.attr({ | |
x1: function(d) { return xScale(3)}, | |
y1: function(d) { return margin.top - 20}, | |
x2: function(d) { return xScale(3)}, | |
y2: function(d) { return chartHeight - margin.bottom + 10}, | |
stroke: "#d1d1d1", | |
"stroke-dasharray": "4 4" | |
}) | |
// Interviewees Scatter Plot -------------------------------- | |
var meanSquares = chartg.selectAll("g.mean") | |
.data(meanData, function(d) { return d.index}) | |
var mse = meanSquares.enter().append("g").classed("mean", true) | |
mse.append("rect") | |
mse.append("path") | |
meanSquares.attr({ | |
transform: function(d) { | |
var off1 = d.offset % 3; | |
off1 *= squareWidth; | |
var off2 = Math.floor((d.offset-1)/3) * squareHeight; | |
var x = xScale(d.mean) - off1 + squareWidth/2; | |
var y = yScale(d.stddev) - squareHeight/2 - off2; | |
return "translate(" + [x,y] + ")scale(" + scale + ")" | |
}, | |
}) | |
meanSquares.select("rect") | |
.attr({ | |
x: 0, | |
y: 0, | |
width: squareWidth/scale, | |
height: squareHeight/scale, | |
fill: "#fff", | |
"fill-opacity": 0.5, | |
}) | |
.on("click", function(d) { | |
console.log("clicked", d); | |
}).on("mouseover", function(d) { | |
console.log(d.index, d.mean, d.stddev); | |
fade(); | |
unfade(d); | |
clearForce(); | |
addForceNodes(d); | |
}) | |
.on("mouseout", function(d) { | |
unfade(); | |
clearForce(); | |
}) | |
meanSquares.select("path") | |
.attr({ | |
d: personIcon, | |
"pointer-events": "none", | |
fill: function(d) { return colorScale(d.mean)} | |
}) | |
forceg.moveToFront(); | |
} | |
render(width, height); | |
function resize() { | |
var container = d3.select("#chart-container").node() | |
var bbox = container.getBoundingClientRect(); | |
console.log("width", bbox.width) | |
chartWidth = bbox.width; | |
render(); | |
} | |
d3.select(window).on("resize", resize); | |
resize(); | |
// END OF THE d3.json call. everything depending on the data should be defined above this line | |
}) | |
// HIGHLIGHTING FUNCTION | |
function clearHighlight() { | |
unfade(); | |
clearForce(); | |
} | |
var highlightAttr = { | |
//opacity: 0.6, | |
//fill: function(c) { return colorScale(c.mean)} | |
fill: function(c) { return "#111"} | |
} | |
function highlightHighDeviation() { | |
fade(); | |
clearForce(); | |
chartg.select("rect.range") | |
.attr({ | |
x: margin.left - squareWidth*2, | |
y: margin.top - squareHeight, | |
width: chartWidth - margin.right - margin.left/2, | |
height: yScale(0.6) - margin.top | |
}) | |
chartg.selectAll("g.mean") | |
.filter(function(d) { return d.stddev >= 0.6}) | |
.select("path") | |
.attr(highlightAttr) | |
.each(function(d){ | |
addForceNodes(d); | |
}) | |
} | |
function highlightLowDeviation() { | |
fade(); | |
clearForce(); | |
chartg.select("rect.range") | |
.attr({ | |
x: margin.left, | |
y: yScale(0) - 50, | |
width: chartWidth - margin.right - margin.left/2 - squareWidth, | |
height: 60 | |
}) | |
chartg.selectAll("g.mean") | |
.filter(function(d) { return d.stddev <= 0}) | |
.select("path") | |
.attr(highlightAttr) | |
.each(function(d){ | |
addForceNodes(d); | |
}) | |
} | |
function highlightAll() { | |
fade(); | |
clearForce(); | |
chartg.selectAll("g.mean") | |
.select("path") | |
.attr(highlightAttr) | |
.each(function(d){ | |
addForceNodes(d); | |
}) | |
} | |
function highlightAtLeastOneScore(category, category2){ | |
fade(); | |
chartg.selectAll("g.mean") | |
.filter(function(d) { | |
var hasCat = false; | |
var hasCat2 = false; | |
d.forEach(function(i) { | |
if(i.score == category) hasCat = true; | |
if(i.score == category2) hasCat2 = true; | |
}) | |
if(category2) return hasCat && hasCat2; | |
return hasCat; | |
}) | |
.select("path") | |
.attr(highlightAttr) | |
.each(function(d){ | |
addForceNodes(d); | |
}) | |
} | |
function highlightScoreRange(low, high){ | |
fade(); | |
chartg.select("rect.range") | |
.attr({ | |
x: xScale(low), | |
y: 10, | |
width: xScale(high) - xScale(low), | |
height: chartHeight - margin.bottom | |
}) | |
chartg.selectAll("g.mean") | |
.filter(function(d) { | |
if(d.mean >= low && d.mean <= high) return true; | |
}) | |
.select("path") | |
.attr(highlightAttr) | |
.each(function(d){ | |
addForceNodes(d); | |
}) | |
} | |
function unfade(d) { | |
var selection = chartg.selectAll("g.mean") | |
if(!d) { | |
selection.select("path") | |
.attr({ | |
opacity: 1, | |
fill: function(c) { return colorScale(c.mean)} | |
}) | |
} else { | |
selection.filter(function(f) { | |
if(f === d) { | |
d3.select(this).select("path").attr({ | |
//opacity: 0.6, | |
fill: function(c) { return colorScale(c.mean)} | |
}) | |
} | |
}) | |
} | |
} | |
function fade() { | |
chartg.selectAll("g.mean").select("path").attr({ | |
fill: "#b7b7b7" | |
}) | |
} | |
// FORCE LAYOUT ----------------------------------- | |
var xrange = xScale.range() | |
var width = Math.abs(xrange[1] - xrange[0]); | |
var yrange = yScale.range(); | |
var height = Math.abs(yrange[0] - yrange[1]); | |
var forceg = chartg.append("g") | |
.attr("transform", "translate(" + [0,0] + ")") | |
var force = d3.layout.force() | |
.size([width, height]) | |
.gravity(0.0) | |
.friction(0.89) | |
.charge(-5) | |
.linkStrength(0) | |
.nodes([]) | |
.links([]) | |
force.start() | |
force.on("tick", function(e) { | |
var k = 0.36 * e.alpha; | |
var nodes = force.nodes(); | |
nodes.forEach(function(t,i) { | |
t.x += (-t.x + t.targetX) * k; | |
t.y += (-t.y + t.targetY) * k; | |
if(t.interviewee){ | |
t.x = t.targetX + squareWidth/2; | |
t.y = t.targetY; | |
} | |
}) | |
forceg.selectAll("circle.node") | |
.attr({ | |
cx: function(d) { return d.x }, | |
cy: function(d) { return d.y } | |
}) | |
forceg.selectAll("line.link") | |
.attr({ | |
x1: function(d) { return d.source.x }, | |
y1: function(d) { return d.source.y }, | |
x2: function(d) { return d.target.x }, | |
y2: function(d) { return d.target.y }, | |
}) | |
}) | |
function clearForce() { | |
forceg.selectAll("circle.node") | |
.remove(); | |
forceg.selectAll("line.link") | |
.remove(); | |
force.links([]) | |
force.nodes([]) | |
chartg.select("rect.range").attr({ | |
width: 0, height: 0 | |
}) | |
} | |
function addForceNodes(interviewee) { | |
var nodes = force.nodes(); | |
var links = force.links(); | |
//console.log("links", links) | |
var evenodd = interviewee.offset % 2 | |
var y = yScale(interviewee.stddev) - interviewee.offset * (1+squareHeight) - squareHeight/2 | |
var x = xScale(interviewee.mean) - squareWidth/2 + evenodd * squareWidth; | |
var off1 = interviewee.offset % 3; | |
off1 *= squareWidth; | |
var off2 = Math.floor((interviewee.offset-1)/3) * squareHeight; | |
var x = xScale(interviewee.mean) - off1 + squareWidth/2; | |
var y = yScale(interviewee.stddev) - squareHeight/2 - off2 + squareHeight/2; | |
//console.log("X,Y", x,y) | |
//generate links between mean "node" and | |
var source = { | |
index: interviewee.index, | |
x: x, | |
y: y, | |
px: x, | |
py: y, | |
targetX: x, | |
targetY: y, | |
mean: interviewee.mean, | |
stddev: interviewee.stddev, | |
interviewee: true, | |
opacity: 0, | |
} | |
nodes.push(source) | |
interviewee.forEach(function(d,i) { | |
var sx = x + 10 * Math.random() + Math.random(); | |
var sy = y + 10 * Math.random() + Math.random(); | |
var node = { | |
index: interviewee.index + "-" + i, | |
px: sx, | |
py: sy, | |
x: sx, | |
y: sy, | |
targetX: xScale(+d.score), | |
targetY: y, | |
mean: interviewee.mean | |
} | |
nodes.push(node) | |
links.push({ | |
index: source.index + "-" + node.index, | |
source: source, | |
//source: 0, | |
target: node | |
}) | |
}) | |
var lines = forceg.selectAll("line.link") | |
.data(links) | |
lines.enter().append("line").classed("link", true) | |
var circles = forceg.selectAll("circle.node") | |
.data(nodes, function(d) { return d.index }) | |
circles.enter().append("circle").classed("node", true) | |
circles.attr({ | |
"pointer-events": "none", | |
r: 4, | |
opacity: function(d) { | |
if(d.opacity || d.opacity === 0) return d.opacity; | |
return 1; | |
}, | |
fill: function(d) { return colorScale(d.mean)} | |
}) | |
force.links(links) | |
force.nodes(nodes); | |
force.start() | |
circles.exit().remove(); | |
} | |
</script> | |
</body> |
[[3,3,4],[3,3,3,3,3,3,4,3],[2,3,3,3,3],[3,3],[2,3,3],[4,2,4],[3,2,3,4,3,2],[3,2,4,3,2,3,2,3,4,3,3,2,4,3,4,4,3],[3,4,3,1,4,3,4,3],[4,4,3],[2,3,2],[3,3,4,3,3,4,4,2,3,4,4],[3,2,2],[2,1,3,3,2,3,2,3],[4,3,3,4,4,4],[3,4,4,3,3,3,4,3,4,2,4],[3,4,3,3,4],[3,2,3,3,2],[2,2,3,3,2,2,4,4,2,3,3],[3,3,4,3,4,4,4],[2,3],[3,3,2,3],[2,2],[3,3,3,3,3,3,2,3,3,3,4,3,2],[1,2,3,2,3,2],[1,3,3,2],[2,2,3,3],[4,3],[3,3],[3,2,2,2,3,3,3,2,2],[4,3,3],[4,3,4],[3,3,3,2,2,3,4],[3,3,4,3,2,4,4,3],[2,4,2,3],[3,4,3],[3,3,4,4,3,2,4],[4,4,4,4],[3,3,3],[4,4],[3,2,4,4,4],[3,4],[3,3,2],[4,4,4],[3,3,3,3,3,3],[3,2],[3,3,2],[3,3],[2,2,3,3,3],[3,4,3,3],[3,3],[2,2],[2,2],[3,4,3,3,3],[2,2],[3,4],[4,4,4,4,4],[4,3],[2,3],[2,2,3],[3,2,4],[2,4,3,3],[3,3],[3,3],[2,3],[4,3],[4,3]] |