I'm trying to add a tooltip on hover to a clip-path [this is my first time using clippath... is that even the right verbiage?]... I think I'm really close [the circle that should follow the line shows up at (0.0) for now] but I seem to be missing something. I was wondering if anyone has time to take a look? I'm trying to adapt code from https://blockbuilder.org/bendoesdata/2c8b315d103bbaf98264efda92d313ab
var data3 = [
{ group: 1, ser1: "2020-01-01", ser2: 3 },
{ group: 1, ser1: "2020-01-02", ser2: 5 },
{ group: 1, ser1: "2020-01-03", ser2: 9 },
{ group: 1, ser1: "2020-01-04", ser2: 3 },
{ group: 1, ser1: "2020-01-05", ser2: 5 },
{ group: 1, ser1: "2020-01-06", ser2: 9 },
{ group: 2, ser1: "2020-01-07", ser2: 10 },
{ group: 2, ser1: "2020-01-08", ser2: 9 },
{ group: 2, ser1: "2020-01-09", ser2: 10 },
{ group: 2, ser1: "2020-01-10", ser2: 20 },
{ group: 2, ser1: "2020-01-11", ser2: 10 },
{ group: 2, ser1: "2020-01-12", ser2: 12 },
{ group: 3, ser1: "2020-01-13", ser2: 20 },
{ group: 3, ser1: "2020-01-14", ser2: 12 },
{ group: 3, ser1: "2020-01-15", ser2: 4 },
{ group: 3, ser1: "2020-01-16", ser2: 22 },
{ group: 3, ser1: "2020-01-17", ser2: 2 },
{ group: 3, ser1: "2020-01-18", ser2: 4 },
]
var line = d3.line()
.x(function (d) { return x(formatDate(d.ser1)); })
.y(function (d) { return y(d.ser2); })
// set the dimensions and margins of the graph
var margin = { top: 10, right: 30, bottom: 30, left: 50 },
width = 1000 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
svg.append("defs")
.append("clipPath")
.attr("id", "chart-path")
.append("rect")
.attr("width", width - 100)
.attr("height", height)
var formatDate = d3.timeParse("%Y-%m-%d");
var bisectDate = d3.bisector(function (d) {
return d.str1;
}).left;
// Initialize a X axis:
var x = d3.scaleTime().range([0, width - 100]);
var xNum = d3.scaleLinear().range([0, width - 100])
var xAxis = d3.axisBottom().scale(x);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "myXaxis")
// Initialize an Y axis
var y = d3.scaleLinear().range([height, 0]);
var yAxis = d3.axisLeft().scale(y);
svg.append("g")
.attr("class", "myYaxis")
// create the Y axis
y.domain([0, d3.max(data3, function (d) { return d.ser2 })]);
svg.selectAll(".myYaxis")
.transition()
.duration(1000)
.call(yAxis);
// Create a update selection: bind to the new data
var u = svg.selectAll(".lineTest")
.data([data3])
.enter()
.append("path")
.attr("class", "lineTest")
.attr("fill", "none")
.attr("stroke", "#0b6dbd")
.attr("stroke-width", 2.5)
.attr("clip-path", "url(#chart-path)")
// Create a function that takes a dataset as input and update the plot:
function update(data) {
// Create the X axis:
x.domain(d3.extent(data, function (d) { return formatDate(d.ser1) }));
svg.selectAll(".myXaxis")
.transition()
.duration(1000)
.call(xAxis);
u.transition()
.duration(1000)
.attr("d", line);
}
// At the beginning, I run the update function on the first dataset:
update(data3)
// this is where I try to implement a tooltip based on
// https://blockbuilder.org/bendoesdata/2c8b315d103bbaf98264efda92d313ab
function drawFocus() {
// Create focus object
let focus = svg.append('g')
.attr('class', 'focus')
// append circle on the line path
focus.append('circle')
.attr('r', 7.5)
// add background rectangle behind the text tooltip
focus.append('rect')
.attr('x', -30)
.attr('y', '-2em')
.attr('width', 70)
.attr('height', 20)
.style("fill", "white");
// add text annotation for tooltip
focus.append('text')
.attr('x', -30)
.attr('dy', '-1em')
.style("fill", "black")
.style("font-family", "SuisseIntl");
focus.append('div')
.attr('x', 10)
.attr('dy', '.35em')
.attr("class", "tooltip")
.style("opacity", 1)
// create an overlay rectangle to draw the above objects on top of
svg.append('rect')
.attr('class', 'overlay')
.attr('width', width - 100)
.attr('height', height)
.on('mouseover', () => focus.style('display', null))
.on('mouseout', () => focus.style('display', 'none'))
.on('mousemove', tipMove);
// make the overlay rectangle transparent,
// so it only serves the purpose of detecting mouse events
d3.select('.overlay')
.style('fill', 'none')
.style('pointer-events', 'all');
// select focus objects and set opacity
d3.selectAll('.focus')
.style('opacity', 0.9);
// select the circle and style it
d3.selectAll('.focus circle')
.style("fill", '#FDD511')
.style("opacity", 0)
// function that adds tooltip on hover
function tipMove() {
// below code finds the date by bisecting and
// stores the x and y coordinate as variables
let x0 = x.invert(d3.mouse(this)[0]);
let i = bisectDate(data3, x0, 1);
let d0 = data3[i - 1];
let d1 = data3[i];
let d = x0 - d0.str1 > d1.str1 - x0 ? d1 : d0;
// place the focus objects on the same path as the line
focus.attr('transform', `translate(${x(d.date)}, ${y(d.value)})`);
// position the x line
focus.select('line.x')
.attr('x1', 0)
.attr('x2', x(formatDate(d.str1)))
.attr('y1', 0)
.attr('y2', 0);
// position the y line
focus.select('line.y')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', 0)
.attr('y2', height - y(d.str2));
// position the text
focus.select('text').text(d.str2).transition() // slowly fade in the tooltip
.duration(100)
.style("opacity", 1);
// show the circle on the path
focus.selectAll('.focus circle')
.style("opacity", 1)
};
}
drawFocus();
.tooltip {
position: absolute;
text-align: left;
font-family: "Open Sans Condensed";
font-size: 12px;
width: 80px;
height: 52px;
padding: 8px;
background: white;
pointer-events: none;
background-color: white;
line-height: 0.05em;
padding: 20px;
/* border: solid;
border-width: 1px; */
}
#my_dataviz {
padding: 20px;
}
<script src="https://d3js.org/d3.v5.js"></script>
<div id="my_dataviz"></div>
You confused your naming. ser1
you called str1
a lot of times. This is a clear sign that your original naming ser1
and ser2
is not logical enough! If you can't follow it now, how about in a year when you revisit the code?
Parse your data before using it, not when you use it. If you store a date as a string in the original object, parse it before doing stuff with it. Otherwise you'll run into problems when you want to compare, plot, format, or just use it.
That's it, most changes I made were just to replace formatDate
when needed with parseDate
(these are different things) at the beginning, and replacing str1/2
with ser1/2
wherever needed.
var data3 = [
{ group: 1, ser1: "2020-01-01", ser2: 3 },
{ group: 1, ser1: "2020-01-02", ser2: 5 },
{ group: 1, ser1: "2020-01-03", ser2: 9 },
{ group: 1, ser1: "2020-01-04", ser2: 3 },
{ group: 1, ser1: "2020-01-05", ser2: 5 },
{ group: 1, ser1: "2020-01-06", ser2: 9 },
{ group: 2, ser1: "2020-01-07", ser2: 10 },
{ group: 2, ser1: "2020-01-08", ser2: 9 },
{ group: 2, ser1: "2020-01-09", ser2: 10 },
{ group: 2, ser1: "2020-01-10", ser2: 20 },
{ group: 2, ser1: "2020-01-11", ser2: 10 },
{ group: 2, ser1: "2020-01-12", ser2: 12 },
{ group: 3, ser1: "2020-01-13", ser2: 20 },
{ group: 3, ser1: "2020-01-14", ser2: 12 },
{ group: 3, ser1: "2020-01-15", ser2: 4 },
{ group: 3, ser1: "2020-01-16", ser2: 22 },
{ group: 3, ser1: "2020-01-17", ser2: 2 },
{ group: 3, ser1: "2020-01-18", ser2: 4 },
];
var parseDate = d3.timeParse("%Y-%m-%d");
// Parse the date ASAP
data3 = data3.map(function(d) {
return {
group: d.group,
ser1: parseDate(d.ser1),
ser2: d.ser2
};
});
var line = d3.line()
.x(function (d) { return x(d.ser1); })
.y(function (d) { return y(d.ser2); })
// set the dimensions and margins of the graph
var margin = { top: 10, right: 30, bottom: 30, left: 50 },
width = 1000 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
svg.append("defs")
.append("clipPath")
.attr("id", "chart-path")
.append("rect")
.attr("width", width - 100)
.attr("height", height)
var formatDate = d3.timeParse("%Y-%m-%d");
var bisectDate = d3.bisector(function (d) {
return d.ser1;
}).left;
// Initialize a X axis:
var x = d3.scaleTime()
.range([0, width - 100])
.domain(d3.extent(data3, function (d) { return d.ser1; }));
var xNum = d3.scaleLinear().range([0, width - 100])
var xAxis = d3.axisBottom().scale(x);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "myXaxis")
// Initialize an Y axis
var y = d3.scaleLinear().range([height, 0]);
var yAxis = d3.axisLeft().scale(y);
svg.append("g")
.attr("class", "myYaxis")
// create the Y axis
y.domain([0, d3.max(data3, function (d) { return d.ser2 })]);
svg.selectAll(".myYaxis")
.transition()
.duration(1000)
.call(yAxis);
// Create a update selection: bind to the new data
var u = svg.selectAll(".lineTest")
.data([data3])
.enter()
.append("path")
.attr("class", "lineTest")
.attr("fill", "none")
.attr("stroke", "#0b6dbd")
.attr("stroke-width", 2.5)
.attr("clip-path", "url(#chart-path)")
svg.selectAll(".myXaxis")
.call(xAxis);
u.attr("d", line);
// this is where I try to implement a tooltip based on
// https://blockbuilder.org/bendoesdata/2c8b315d103bbaf98264efda92d313ab
function drawFocus() {
// Create focus object
let focus = svg.append('g')
.attr('class', 'focus')
// append circle on the line path
focus.append('circle')
.attr('r', 7.5)
// add background rectangle behind the text tooltip
focus.append('rect')
.attr('x', -30)
.attr('y', '-2em')
.attr('width', 70)
.attr('height', 20)
.style("fill", "white");
// add text annotation for tooltip
focus.append('text')
.attr('x', -30)
.attr('dy', '-1em')
.style("fill", "black")
.style("font-family", "SuisseIntl");
focus.append('div')
.attr('x', 10)
.attr('dy', '.35em')
.attr("class", "tooltip")
.style("opacity", 1)
// create an overlay rectangle to draw the above objects on top of
svg.append('rect')
.attr('class', 'overlay')
.attr('width', width - 100)
.attr('height', height)
.on('mouseover', () => focus.style('display', null))
.on('mouseout', () => focus.style('display', 'none'))
.on('mousemove', tipMove);
// make the overlay rectangle transparent,
// so it only serves the purpose of detecting mouse events
d3.select('.overlay')
.style('fill', 'none')
.style('pointer-events', 'all');
// select focus objects and set opacity
d3.selectAll('.focus')
.style('opacity', 0.9);
// select the circle and style it
d3.selectAll('.focus circle')
.style("fill", '#FDD511')
.style("opacity", 0)
// function that adds tooltip on hover
function tipMove() {
// below code finds the date by bisecting and
// stores the x and y coordinate as variables
let x0 = x.invert(d3.mouse(this)[0]);
let i = bisectDate(data3, x0, 1);
let d0 = data3[i - 1];
let d1 = data3[i];
let d = x0 - d0.ser1 > d1.ser1 - x0 ? d1 : d0;
// place the focus objects on the same path as the line
focus.attr('transform', `translate(${x(d.ser1)}, ${y(d.ser2)})`);
// position the x line
focus.select('line.x')
.attr('x1', 0)
.attr('x2', x(d.ser1))
.attr('y1', 0)
.attr('y2', 0);
// position the y line
focus.select('line.y')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', 0)
.attr('y2', height - y(d.ser2));
// position the text
focus.select('text').text(d.ser2)
.transition() // slowly fade in the tooltip
.duration(100)
.style("opacity", 1);
// show the circle on the path
focus.selectAll('.focus circle')
.style("opacity", 1)
};
}
drawFocus();
.tooltip {
position: absolute;
text-align: left;
font-family: "Open Sans Condensed";
font-size: 12px;
width: 80px;
height: 52px;
padding: 8px;
background: white;
pointer-events: none;
background-color: white;
line-height: 0.05em;
padding: 20px;
/* border: solid;
border-width: 1px; */
}
#my_dataviz {
padding: 20px;
}
<script src="https://d3js.org/d3.v5.js"></script>
<div id="my_dataviz"></div>