I'm working on creating a tooltip in a D3 line chart. A tutorial I was following (https://www.youtube.com/watch?v=uyPYxx-WGxc) gave me some code, but the tutorial handled data differently than I have. In my code, the data is bound to the chart line, not to the entirety of the D3 code.
I think I got the data also bound to the rectangle, but I'm not sure. I put in the tutorial's code for the listeningRect mouseover method, but when I run the page, the variable xPos is the same no matter where I put my mouse.
What am I doing wrong please? Did I handle the data binding improperly?
See my code: https://codepen.io/lschneiderman/pen/VYemXyv
html:
<div id="container"></div>
css:
.x.label, .y.label {
font-weight: bold;
font-size: 1rem;
}
.x.label {
margin-left: -50%;
}
rect {
pointer-events: all;
fill-opacity: 0;
stroke-opacity: 0;
z-index: 1;
background-color: transparent;
}
.tooltip {
position: absolute;
padding: 10px;
background-color: steelblue;
color: white;
border: 1px solid white;
border-radius: 10px;
display: none;
opacity: .5;
}
js:
var data = [ {'year':'2007', 'percent':'.05'}, {'year':'2008', 'percent':'.10'}, {'year':'2009', 'percent':'.15'}, {'year':'2010', 'percent':'.25'} ];
sendData(data);
function sendData(data) {
const width = 1140;
const height = 400;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 50;
const marginLeft = 70;
//var data = [ {'year':'2007', 'percent':'.05'}, {'year':'2008', 'percent':'.10'}, {'year':'2009', 'percent':'.15'} ]
const x = d3.scaleUtc()
.domain([new Date("2007-01-01"), new Date("2023-01-01")])
.range([marginLeft, width - marginRight]); //same as .range([70,1120])
const y = d3.scaleLinear()
.domain([0, .5])
.range([height - marginBottom, marginTop]); //same as .range([350, 20])
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x)
.ticks(d3.utcYear.every(1))
);
svg.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", width/2)
.attr("y", height - 6)
.text("Year");
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("x", -height/3)
.attr("y", 6)
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.text("Percent");
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y));
var heightOfY = height-marginBottom;
var Xmin = 2007;
var Xmax = 2023;
var numTicks = Xmax - Xmin + 1; //have to count the 0 point, so add 1
var XmaxRange = width - marginRight;
var eachTickWidth = XmaxRange / numTicks;
let path=svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) {
//return x(d.year)
var mine = ((d.year - Xmin)*eachTickWidth) + marginLeft;
//return x(d.year);
return(mine); console.log(d.year);
})
.y(function(d) {
return y(d.percent)
})
);
const tooltip = d3.select("svg")
.append("div")
.attr("class", "tooltip");
const circle = svg.append("circle")
.attr("r", 0)
.attr("fill","steelblue")
.style("stroke","white")
.attr("opacity", .70)
.style("pointer-events","none");
const listeningRect = svg.append("rect")
.datum(data)
.attr("width", width)
.attr("height", height);
listeningRect.on("mousemove", function(event){
const [xCoord] = d3.pointer(event, this);
const bisectData = d3.bisector(d => data.year).left;
const x0 = x.invert(xCoord);
const i = bisectData(data, x0, 1);
const d0 = data[i-1];
const d1 = data[i];
const d = x0 - d0.year> d1.year- x0 ? d1 : d0;
const xPos = x(d.year);
const yPos = y(d.percent);
circle.attr("cx", xPos)
.attr("cy", yPos);
console.log(xPos); //always gives the same variable
});
container.append(svg.node());
} //end function sendData
The problem is that your data has the year number for the x axis value,
while the axis itself being scaleUtc, will have its values as Date objects.
In general, I would recommend that you use the right datatypes
for your data points. It's lucky that the heuristic algorithm in d3 will
transform the string '2007' to the Date object for 01-Jan-2007.
Also, I find using strings instead of numbers
({percent: '.05'} instead of {percent: .05}) problematic - it may create
subtle errors that are hard to detect or debug.
Coming back to the code in the OP, a solution is, that when making
axis-based calculations, to convert the data point to a Date:
const dataToDate = d => new Date(d.year, 0, 1);
listeningRect.on("mousemove", function(event){
const [xCoord] = d3.pointer(event, this);
const bisectData = d3.bisector(d => dataToDate(d)).left;
const x0 = x.invert(xCoord);
const i = bisectData(data, x0, 1);
const d0 = data[i-1];
const d1 = data[i]; // d1 might be undefined
const d = d1 && (x0 - dataToDate(d0) > dataToDate(d1) - x0) ? d1 : d0;
const xPos = x(dataToDate(d));
const yPos = y(d.percent);
circle.attr("cx", xPos)
.attr("cy", yPos);
});
The original code with these modification in a jsFiddle (stack snippet doesn't work for this piece of code).