Reputation: 59
I'm pretty new to D3 and just incorporated the below tooltips into my application. I have both a single line chart as well as a multi line chart.
Single line: https://bl.ocks.org/alandunning/cfb7dcd7951826b9eacd54f0647f48d3
Multi Line: Multiseries line chart with mouseover tooltip
As you can see, the functionality of the two tooltips are different. The Single Line tooltip jumps from each data point whereas the Multi Line continually follows the chart path. I want to change the Multi Line functionality to mimic how the Single Line tooltip works.
Any help would be greatly appreciated. Please let me know if I need to provide more information. Also note that the data I'm working with is an array of arrays
Below is my code:
Single Line Chart:
let g = svg.append('g');
g.append("path")
.datum(this.dataObj)
.attr("class",`line-${this.yAxisData} line`)
.attr('d', line)
.attr("stroke",`${this.color(this.dataObj.label)}`)
.attr("fill",'none')
.attr("transform", `translate(${this.margin.left},${this.margin.top})`);
var focus = g.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("line")
.datum(this.dataObj)
.attr("class", "x-hover-line hover-line")
.attr("transform",`translate(${this.margin.left},${this.margin.top})`)
.attr("stroke",`${this.color(this.dataObj.label)}`)
.attr("y1", 0)
.attr("y2", height);
focus.append("circle")
.datum(this.dataObj)
.attr("transform",`translate(${this.margin.left},${this.margin.top})`)
.attr("stroke",`${this.color(this.dataObj.label)}`)
.attr("r", 7.5);
focus.append("text")
.attr("class","linetip")
.attr("x", 40)
.attr("dy", "0.5em");
svg.append("rect")
.attr("transform", `translate(${this.margin.left},${this.margin.top})`)
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function() { focus.style("display", null); })
.on("mouseout", function() { focus.style("display", "none"); })
.on("mousemove", this.mousemove);
mousemove() {
var bisectDate = d3.bisector(function(d) { return d.date; }).left;
let mouse = d3.mouse(d3.event.currentTarget);
let svg = d3.select(this.container);
var x0 = this.x.invert(mouse[0]);
var i = bisectDate(this.dataObj, x0);
var d0 = this.dataObj[i - 1];
var d1 = this.dataObj[i];
var d = x0 - d0.date > d1.date - x0 ? d1 : d0;
var focus = svg.select(".focus");
focus.attr("transform", "translate(" + this.x(d[this.xAxisData]) + "," + this.y(d[this.yAxisData]) + ")");
focus.select("text").text(`[${d[this.yAxisData]}]`);
focus.select(".x-hover-line").attr("y2", this.height - this.y(d[this.yAxisData]));
focus.select(".y-hover-line").attr("x2", this.width + this.width);
}
Multi Line Chart:
//append paths
let g = svg.append('g');
let chartLines = g.selectAll('.lines')
.data(this.dataObj)
.enter()
.append('g')
.attr('class', 'lines');
chartLines.append('path')
.attr('class','line')
.attr('d', d => {
return line(d);
})
.attr('stroke', (d) => color(d[0].label))
.attr('fill','none')
.attr("transform", `translate(${this.margin.left},0)`);
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", "2px")
.style("stroke-dasharray", "3,3")
.style("opacity", "0");
var mousePerLine = mouseG.selectAll('.mouse-per-line')
.data(this.dataObj)
.enter()
.append("g")
.attr("class", "mouse-per-line");
mousePerLine.append("circle")
.datum(d=>{return d})
.attr("r", 7)
.attr("stroke", (d,i) => {
console.log(d)
return `${this.color(d[i].label)}`
})
.style("fill", "none")
.style("opacity", "0");
mousePerLine.append("text")
.datum(d=>{return d})
.attr("transform", "translate(10,3)");
mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
.attr("transform", `translate(${this.margin.left},0)`)
.attr('width', width) // can't catch mouse events on a g element
.attr('height', height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.on('mouseout', () => { // 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', () => { // 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', () => {
let mouse = d3.mouse(d3.event.currentTarget);
d3.select(".mouse-line")
.attr("d", () => {
var d = "M" + mouse[0] + "," + height;
d += " " + mouse[0] + "," + 0;
return d;
});
d3.selectAll(".mouse-per-line")
.attr("transform", (d, i) => {
var lines = document.getElementsByClassName('line')
var xDate = this.x.invert(mouse[0])
var bisect = d3.bisector(function(d) { return d.date; }).right;
var idx = bisect(this.dataObj, xDate);
var beginning = 0,
end = lines[i].getTotalLength()
var target = null;
while (true){
var target = Math.floor((beginning + end) / 2);
var 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('text')
.text(this.y.invert(pos.y));
return "translate(" + mouse[0] + "," + pos.y +")";
});
});
Upvotes: 2
Views: 7725
Reputation: 748
I took Mark's answer as a reference from the Multiseries line chart with mouseover tooltip you provided.
Basically, what you need to do is set the tooltips to show on each tick of the x-axis data, so instead of grabbing the position of the mouse with mouse[0] and moving the tooltips, you should move it to the position where the x-axis data is.
Here's the detail of the changes I made:
mouseG.append('svg:rect')
.attr('width', width)
.attr('height', height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.on('mouseout', () => {
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', () => {
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', () => {
let mouse = d3.mouse(d3.event.currentTarget);
// MOVE THIS BEFORE THE RETURN
// d3.select(".mouse-line")
// .attr("d", () => {
// var d = "M" + mouse[0] + "," + height;
// d += " " + mouse[0] + "," + 0;
// return d;
// });
d3.selectAll(".mouse-per-line")
.attr("transform", (d, i) => {
var lines = document.getElementsByClassName('line')
var xDate = this.x.invert(mouse[0])
var bisect = d3.bisector(function(d) { return d.date; }).right;
var idx = bisect(this.dataObj, xDate);
// GET RID OF THIS
// var beginning = 0,
// end = lines[i].getTotalLength()
// var target = null;
// while (true){
// var target = Math.floor((beginning + end) / 2);
// var 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
// }
// REPLACE pos.y WITH y(d.values[idx].temperature)
// AND mouse[0] WITH x(d.values[idx].date)
d3.select('text')
.text(this.y.invert(pos.y));
return "translate(" + mouse[0] + "," + pos.y +")";
});
});
Below is the fully working code with the changes applied. For this snippet I used interpolate('linear')
to show the values correctly; if you use interpolate('basis')
, the tooltips and lines will not match correctly:
<!DOCTYPE html>
<html>
<head>
<script data-require="[email protected]" data-semver="3.5.3" src="http://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 = 400 - margin.left - margin.right,
height = 250 - 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("linear")
.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.selectAll(".mouse-per-line")
.attr("transform", function (d, i) {
var xDate = x.invert(mouse[0]),
bisect = d3.bisector(function (d) { return d.date; }).left;
idx = bisect(d.values, xDate);
d3.select(this).select('text')
.text(y.invert(y(d.values[idx].temperature)).toFixed(2));
d3.select(".mouse-line")
.attr("d", function () {
var data = "M" + x(d.values[idx].date) + "," + height;
data += " " + x(d.values[idx].date) + "," + 0;
return data;
});
return "translate(" + x(d.values[idx].date) + "," + y(d.values[idx].temperature) + ")";
});
});
</script>
</body>
</html>
Upvotes: 5