Using data from the US Census, this visualization displays popular names that have historically been androgynous.
Control points are not intersected by the path for several non-linear interpolations for lines ("basis" in this case). Because of this, point tracking for non-linear lines should be calculated from the svg-path rather than from the d3-scales. Mike Bostock has two examples showing how to do this with a single path. This gist shows one approach for point tracking with multiple paths. Other approaches are shown here and here.
This approach uses on-the-fly closestPoint to select both the line and point to highlight. Each move the mouse requires computing the distance to each point on the chart, thus the noted performance difference for differing number of lines shown.
xxxxxxxxxx
<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="https://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)
.orient("bottom"));
svg.append("g")
.attr("class", "axis axis--y")
.call(d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10, "%"))
.append("text")
.attr("x", 4)
.attr("dy", ".32em")
.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 line(d.values); });
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 line(d.values); });
}
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)]);
return line(d.values.slice(zero_counter, d.values.length));
}
function closestPoint(pathNode, point) {
var pathLength = pathNode.getTotalLength(),
numberOfItems = pathNode.getPathSegAtLength(pathLength),
precision = (pathLength / 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>
Modified http://d3js.org/d3.v3.js to a secure url
https://d3js.org/d3.v3.js