|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
body { |
|
width: 960px; |
|
margin: auto; |
|
position: relative; |
|
} |
|
|
|
svg { |
|
font: 11px "Helvetica Neue", Helvetica, Arial, sans-serif; |
|
} |
|
|
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.axis--y path { |
|
display: none; |
|
} |
|
|
|
.androgs { |
|
fill: none; |
|
stroke: #aaa; |
|
stroke-linejoin: round; |
|
stroke-linecap: round; |
|
stroke-width: 1.5px; |
|
} |
|
|
|
.androg--hover { |
|
stroke: #000; |
|
} |
|
|
|
.focus text { |
|
text-anchor: middle; |
|
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; |
|
} |
|
|
|
#form { |
|
position: absolute; |
|
top: 20px; |
|
right: 30px; |
|
} |
|
|
|
rect { |
|
fill: none; |
|
pointer-events: all; |
|
} |
|
|
|
</style> |
|
<label id="form"> |
|
Display |
|
<select id="form_select"> |
|
</select> |
|
Lines |
|
</label> |
|
<script src="http://d3js.org/d3.v3.js"></script> |
|
<script> |
|
|
|
var years, |
|
yearFormat = d3.time.format("%Y"); |
|
|
|
var margin = {top: 20, right: 30, bottom: 30, left: 40}, |
|
width = 960 - margin.left - margin.right, |
|
height = 500 - margin.top - margin.bottom; |
|
|
|
var x = d3.time.scale() |
|
.range([0, width]); |
|
|
|
var y = d3.scale.linear() |
|
.range([height, 0]); |
|
|
|
var color = d3.scale.category20(); |
|
|
|
var line = d3.svg.line() |
|
.interpolate("basis") |
|
.x(function(d) { return x(d.date); }) |
|
.y(function(d) { return y(d.value); }); |
|
|
|
var focus; |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
svg.append("rect") |
|
.attr("width", width) |
|
.attr("height", height) |
|
.on("mousemove", mousemoved); |
|
|
|
var paths = [], |
|
shown_paths = []; |
|
d3.csv("steady_andro.csv", type, function(error, androgs) { |
|
x.domain(d3.extent(years)); |
|
y.domain([0, d3.max(androgs, function(c) { return d3.max(c.values, function(d) { return d.value; }); })]).nice(); |
|
|
|
svg.append("g") |
|
.attr("class", "axis axis--x") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(d3.svg.axis() |
|
.scale(x) |
|
.tickValues([]) |
|
.orient("bottom")) |
|
.append("text") |
|
.attr("dx", "11px") |
|
.attr("dy", "11px") |
|
.style("font-weight", "bold") |
|
.text("Year First Appeared"); |
|
|
|
svg.append("g") |
|
.attr("class", "axis axis--y") |
|
.call(d3.svg.axis() |
|
.scale(y) |
|
.orient("left") |
|
.ticks(10, "%")) |
|
.append("text") |
|
.attr("x", -75) |
|
.attr("transform", "rotate(-90)") |
|
.attr("dy", "10px") |
|
.style("font-weight", "bold") |
|
.text("Percent Female"); |
|
|
|
svg.append("g") |
|
.attr("class", "androgs") |
|
.selectAll("path") |
|
.data(androgs) |
|
.enter().append("path") |
|
.attr("d", function(d) { d.line = this; return removedStartingZeroes(d); }) |
|
|
|
focus = svg.append("g") |
|
.attr("transform", "translate(-100,-100)") |
|
.attr("class", "focus"); |
|
|
|
focus.append("circle") |
|
.attr("r", 3.5); |
|
|
|
focus.append("text") |
|
.attr("y", -10); |
|
|
|
var form_select = d3.select("#form_select"); |
|
|
|
form_select.selectAll("option") |
|
.data(androgs) |
|
.enter().append("option") |
|
.attr("value", function(d, i) { return i+1; }) |
|
.text(function(d, i) { return i+1; }) |
|
|
|
// set the last option as selected |
|
var all_options = d3.selectAll("option")[0]; |
|
d3.select(all_options[all_options.length - 1]) |
|
.attr("selected", "selected"); |
|
|
|
form_select.on("change", change); |
|
|
|
paths = androgs; |
|
shown_paths = paths; |
|
}); |
|
|
|
function change() { |
|
var n_lines = d3.select("#form_select").property("value"); |
|
shown_paths = paths.slice(0, n_lines); |
|
|
|
var parent_g = svg.select(".androgs"); |
|
|
|
var rendered_paths = parent_g.selectAll("path") |
|
.data(shown_paths); |
|
|
|
rendered_paths.exit().remove(); |
|
|
|
rendered_paths.enter().append("path") |
|
.attr("d", function(d) { d.line = this; return removedStartingZeroes(d); }); |
|
} |
|
|
|
function mousemoved() { |
|
d3.select(".androg--hover").classed("androg--hover", false); |
|
|
|
var m = d3.mouse(this), |
|
min_path_index = 0; |
|
for (var i = 0; i < shown_paths.length; i++) { |
|
shown_paths[i].distance = closestPoint(shown_paths[i].line, m); |
|
if (shown_paths[i].distance.distance < shown_paths[min_path_index].distance.distance) { |
|
min_path_index = i; |
|
} |
|
}; |
|
|
|
var min_path = shown_paths[min_path_index]; |
|
d3.select(min_path.line).classed("androg--hover", true); |
|
//min_path.line.parentNode.appendChild(min_path.line); |
|
focus.attr("transform", "translate(" + min_path.distance[0] + "," + min_path.distance[1] + ")"); |
|
focus.select("text").text(min_path.name); |
|
} |
|
|
|
function removedStartingZeroes(d) { |
|
var zero_counter = 0; |
|
var j = 0; |
|
while (j < d.values.length) { |
|
if (d.values[j].value != 0.0) { |
|
break; |
|
} |
|
j++; |
|
zero_counter++; |
|
} |
|
min_datestr = 'Jan 1 ' + (1880 + zero_counter).toString(); |
|
max_datestr = 'Jan 1 ' + "2013"; |
|
x.domain([new Date(min_datestr), new Date(max_datestr)]); |
|
|
|
var normalized_path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); |
|
normalized_path.setAttribute('d', line(d.values.slice(zero_counter, d.values.length))); |
|
return line(d.values.slice(zero_counter, d.values.length)); |
|
} |
|
|
|
function closestPoint(pathNode, point) { |
|
var pathLength = pathNode.getTotalLength(), |
|
precision = pathLength / pathNode.pathSegList.numberOfItems * .5, |
|
best, |
|
bestLength, |
|
bestDistance = Infinity; |
|
|
|
// linear scan for coarse approximation |
|
for (var scan, scanLength = 0, scanDistance; scanLength <= pathLength; scanLength += precision) { |
|
if ((scanDistance = distance2(scan = pathNode.getPointAtLength(scanLength))) < bestDistance) { |
|
best = scan, bestLength = scanLength, bestDistance = scanDistance; |
|
} |
|
} |
|
|
|
// binary search for precise estimate |
|
precision *= .5; |
|
while (precision > .5) { |
|
var before, |
|
after, |
|
beforeLength, |
|
afterLength, |
|
beforeDistance, |
|
afterDistance; |
|
if ((beforeLength = bestLength - precision) >= 0 && (beforeDistance = distance2(before = pathNode.getPointAtLength(beforeLength))) < bestDistance) { |
|
best = before, bestLength = beforeLength, bestDistance = beforeDistance; |
|
} else if ((afterLength = bestLength + precision) <= pathLength && (afterDistance = distance2(after = pathNode.getPointAtLength(afterLength))) < bestDistance) { |
|
best = after, bestLength = afterLength, bestDistance = afterDistance; |
|
} else { |
|
precision *= .5; |
|
} |
|
} |
|
|
|
best = [best.x, best.y]; |
|
best.distance = Math.sqrt(bestDistance); |
|
return best; |
|
|
|
function distance2(p) { |
|
var dx = p.x - point[0], |
|
dy = p.y - point[1]; |
|
return dx * dx + dy * dy; |
|
} |
|
} |
|
|
|
function type(d, i) { |
|
if (!i) years = Object.keys(d).map(yearFormat.parse).filter(Number); |
|
var androg = { |
|
name: d.name.replace(/ (msa|necta div|met necta|met div)$/i, ""), |
|
values: null |
|
}; |
|
androg.values = years.map(function(m) { |
|
return { |
|
androg: androg, |
|
date: m, |
|
value: d[yearFormat(m)] |
|
}; |
|
}); |
|
return androg; |
|
} |
|
|
|
</script> |