Multiseries line chart with mouseover tooltip
Solution 1:
The question you referenced I answered back in April. Since then, I've learned a bit more about SVG
and d3
, so I'll let this serve as an update to that answer.
Note, I borrowed a bit of code from @Duopixel's excellent code sample here.
Here's the commented particulars:
// append a g for all the mouse over nonsense
var mouseG = svg.append("g")
.attr("class", "mouse-over-effects");
// this is the vertical line
mouseG.append("path")
.attr("class", "mouse-line")
.style("stroke", "black")
.style("stroke-width", "1px")
.style("opacity", "0");
// keep a reference to all our lines
var lines = document.getElementsByClassName('line');
// here's a g for each circle and text on the line
var mousePerLine = mouseG.selectAll('.mouse-per-line')
.data(cities)
.enter()
.append("g")
.attr("class", "mouse-per-line");
// the circle
mousePerLine.append("circle")
.attr("r", 7)
.style("stroke", function(d) {
return color(d.name);
})
.style("fill", "none")
.style("stroke-width", "1px")
.style("opacity", "0");
// the text
mousePerLine.append("text")
.attr("transform", "translate(10,3)");
// rect to capture mouse movements
mouseG.append('svg:rect')
.attr('width', width)
.attr('height', height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.on('mouseout', function() { // on mouse out hide line, circles and text
d3.select(".mouse-line")
.style("opacity", "0");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "0");
d3.selectAll(".mouse-per-line text")
.style("opacity", "0");
})
.on('mouseover', function() { // on mouse in show line, circles and text
d3.select(".mouse-line")
.style("opacity", "1");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "1");
d3.selectAll(".mouse-per-line text")
.style("opacity", "1");
})
.on('mousemove', function() { // mouse moving over canvas
var mouse = d3.mouse(this);
// move the vertical line
d3.select(".mouse-line")
.attr("d", function() {
var d = "M" + mouse[0] + "," + height;
d += " " + mouse[0] + "," + 0;
return d;
});
// position the circle and text
d3.selectAll(".mouse-per-line")
.attr("transform", function(d, i) {
console.log(width/mouse[0])
var xDate = x.invert(mouse[0]),
bisect = d3.bisector(function(d) { return d.date; }).right;
idx = bisect(d.values, xDate);
// since we are use curve fitting we can't relay on finding the points like I had done in my last answer
// this conducts a search using some SVG path functions
// to find the correct position on the line
// from http://bl.ocks.org/duopixel/3824661
var beginning = 0,
end = lines[i].getTotalLength(),
target = null;
while (true){
target = Math.floor((beginning + end) / 2);
pos = lines[i].getPointAtLength(target);
if ((target === end || target === beginning) && pos.x !== mouse[0]) {
break;
}
if (pos.x > mouse[0]) end = target;
else if (pos.x < mouse[0]) beginning = target;
else break; //position found
}
// update the text with y value
d3.select(this).select('text')
.text(y.invert(pos.y).toFixed(2));
// return position
return "translate(" + mouse[0] + "," + pos.y +")";
});
});
Full working code:
<!DOCTYPE html>
<html>
<head>
<script data-require="[email protected]" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
</style>
</head>
<body>
<script>
var myData = "date New York San Francisco Austin\n\
20111001 63.4 62.7 72.2\n\
20111002 58.0 59.9 67.7\n\
20111003 53.3 59.1 69.4\n\
20111004 55.7 58.8 68.0\n\
20111005 64.2 58.7 72.4\n\
20111006 58.8 57.0 77.0\n\
20111007 57.9 56.7 82.3\n\
20111008 61.8 56.8 78.9\n\
20111009 69.3 56.7 68.8\n\
20111010 71.2 60.1 68.7\n\
20111011 68.7 61.1 70.3\n\
20111012 61.8 61.5 75.3\n\
20111013 63.0 64.3 76.6\n\
20111014 66.9 67.1 66.6\n\
20111015 61.7 64.6 68.0\n\
20111016 61.8 61.6 70.6\n\
20111017 62.8 61.1 71.1\n\
20111018 60.8 59.2 70.0\n\
20111019 62.1 58.9 61.6\n\
20111020 65.1 57.2 57.4\n\
20111021 55.6 56.4 64.3\n\
20111022 54.4 60.7 72.4\n";
var margin = {
top: 20,
right: 80,
bottom: 30,
left: 50
},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var parseDate = d3.time.format("%Y%m%d").parse;
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var line = d3.svg.line()
.interpolate("basis")
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.temperature);
});
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 + ")");
var data = d3.tsv.parse(myData);
color.domain(d3.keys(data[0]).filter(function(key) {
return key !== "date";
}));
data.forEach(function(d) {
d.date = parseDate(d.date);
});
var cities = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
date: d.date,
temperature: +d[name]
};
})
};
});
x.domain(d3.extent(data, function(d) {
return d.date;
}));
y.domain([
d3.min(cities, function(c) {
return d3.min(c.values, function(v) {
return v.temperature;
});
}),
d3.max(cities, function(c) {
return d3.max(c.values, function(v) {
return v.temperature;
});
})
]);
var legend = svg.selectAll('g')
.data(cities)
.enter()
.append('g')
.attr('class', 'legend');
legend.append('rect')
.attr('x', width - 20)
.attr('y', function(d, i) {
return i * 20;
})
.attr('width', 10)
.attr('height', 10)
.style('fill', function(d) {
return color(d.name);
});
legend.append('text')
.attr('x', width - 8)
.attr('y', function(d, i) {
return (i * 20) + 9;
})
.text(function(d) {
return d.name;
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Temperature (ºF)");
var city = svg.selectAll(".city")
.data(cities)
.enter().append("g")
.attr("class", "city");
city.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", function(d) {
return color(d.name);
});
city.append("text")
.datum(function(d) {
return {
name: d.name,
value: d.values[d.values.length - 1]
};
})
.attr("transform", function(d) {
return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
var mouseG = svg.append("g")
.attr("class", "mouse-over-effects");
mouseG.append("path") // this is the black vertical line to follow mouse
.attr("class", "mouse-line")
.style("stroke", "black")
.style("stroke-width", "1px")
.style("opacity", "0");
var lines = document.getElementsByClassName('line');
var mousePerLine = mouseG.selectAll('.mouse-per-line')
.data(cities)
.enter()
.append("g")
.attr("class", "mouse-per-line");
mousePerLine.append("circle")
.attr("r", 7)
.style("stroke", function(d) {
return color(d.name);
})
.style("fill", "none")
.style("stroke-width", "1px")
.style("opacity", "0");
mousePerLine.append("text")
.attr("transform", "translate(10,3)");
mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
.attr('width', width) // can't catch mouse events on a g element
.attr('height', height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.on('mouseout', function() { // on mouse out hide line, circles and text
d3.select(".mouse-line")
.style("opacity", "0");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "0");
d3.selectAll(".mouse-per-line text")
.style("opacity", "0");
})
.on('mouseover', function() { // on mouse in show line, circles and text
d3.select(".mouse-line")
.style("opacity", "1");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "1");
d3.selectAll(".mouse-per-line text")
.style("opacity", "1");
})
.on('mousemove', function() { // mouse moving over canvas
var mouse = d3.mouse(this);
d3.select(".mouse-line")
.attr("d", function() {
var d = "M" + mouse[0] + "," + height;
d += " " + mouse[0] + "," + 0;
return d;
});
d3.selectAll(".mouse-per-line")
.attr("transform", function(d, i) {
console.log(width/mouse[0])
var xDate = x.invert(mouse[0]),
bisect = d3.bisector(function(d) { return d.date; }).right;
idx = bisect(d.values, xDate);
var beginning = 0,
end = lines[i].getTotalLength(),
target = null;
while (true){
target = Math.floor((beginning + end) / 2);
pos = lines[i].getPointAtLength(target);
if ((target === end || target === beginning) && pos.x !== mouse[0]) {
break;
}
if (pos.x > mouse[0]) end = target;
else if (pos.x < mouse[0]) beginning = target;
else break; //position found
}
d3.select(this).select('text')
.text(y.invert(pos.y).toFixed(2));
return "translate(" + mouse[0] + "," + pos.y +")";
});
});
</script>
</body>
</html>