Skip to content

Instantly share code, notes, and snippets.

@praveenuics
Created June 22, 2020 06:55
Show Gist options
  • Save praveenuics/21443a18306b4775572cdc0986651eaf to your computer and use it in GitHub Desktop.
Save praveenuics/21443a18306b4775572cdc0986651eaf to your computer and use it in GitHub Desktop.
Styling tooltip in multi-line D3 line chart
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
text.inner-circle {
font-weight: 400;
font-size: 12px;
text-transform: uppercase;
}
text.inner-text {
font-weight: 400;
font-size: 36px;
font-family: 'Metric Regular', 'Metric';
text-align: center;
font-style: normal;
text-transform: uppercase;
}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 2;
shape-rendering: crispEdges;
}
.grid .tick {
stroke: lightgrey;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
}
.grid path {
stroke-width: 0;
}
</style>
</head>
<body>
<script>
// Feel free to change or delete any of the code you see in this editor!
var margin = {
top: 20,
right: 30,
bottom: 30,
left: 40
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var z = d3.scaleOrdinal(d3.schemeCategory10);
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 parseDate = d3.timeFormat("%Y-%m-%dT%H:%M:%S.%LZ");
var data = [{
data: [
[new Date("2016-01-20T05:31:17.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:31:47.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:32:17.000Z").getTime(), 95.4, {}],
[new Date("2016-01-20T05:32:47.000Z").getTime(), 96.1, {}],
[new Date("2016-01-20T05:33:17.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:33:47.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:34:17.000Z").getTime(), 95.5, {}],
[new Date("2016-01-20T05:34:47.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:35:17.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:35:47.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:36:17.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:36:47.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:37:17.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:37:47.000Z").getTime(), 95.5, {}],
[new Date("2016-01-20T05:38:17.000Z").getTime(), 95.4, {}],
[new Date("2016-01-20T05:38:47.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:39:17.000Z").getTime(), 96.0, {}],
[new Date("2016-01-20T05:39:47.000Z").getTime(), 96.1, {}],
[new Date("2016-01-20T05:40:17.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:40:47.000Z").getTime(), 96.0, {}],
[new Date("2016-01-20T05:41:17.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:41:47.000Z").getTime(), 94.9, {}],
[new Date("2016-01-20T05:42:17.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:42:47.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:43:17.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:43:47.000Z").getTime(), 96.0, {}],
[new Date("2016-01-20T05:44:17.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:44:47.000Z").getTime(), 96.0, {}],
[new Date("2016-01-20T05:45:17.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:45:47.000Z").getTime(), 96.0, {}],
[new Date("2016-01-20T05:46:17.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:46:47.000Z").getTime(), 96.0, {}],
[new Date("2016-01-20T05:47:17.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:47:47.000Z").getTime(), 96.2, {}],
[new Date("2016-01-20T05:48:17.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:48:47.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:49:17.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:49:47.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:50:18.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:50:48.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:51:18.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:51:48.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:52:18.000Z").getTime(), 95.5, {}],
[new Date("2016-01-20T05:52:48.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:53:18.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:53:48.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:54:18.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:54:48.000Z").getTime(), 95.9, {}],
[new Date("2016-01-20T05:55:18.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:55:48.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:56:18.000Z").getTime(), 95.6, {}],
[new Date("2016-01-20T05:56:48.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:57:18.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:57:48.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T05:58:18.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:58:48.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:59:18.000Z").getTime(), 95.6, {}],
[new Date("2016-01-20T05:59:48.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T06:00:18.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T06:00:48.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T06:01:18.000Z").getTime(), 95.6, {}],
[new Date("2016-01-20T06:01:48.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T06:02:18.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T06:02:48.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T06:03:18.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T06:03:48.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T06:04:18.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T06:04:48.000Z").getTime(), 95.8, {}],
[new Date("2016-01-20T06:05:18.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T06:05:48.000Z").getTime(), 95.7, {}]
],
label: "a"
}, {
data: [
[new Date("2016-01-20T05:31:17.000Z").getTime(), 90.9, {}],
[new Date("2016-01-20T05:31:47.000Z").getTime(), 91.9, {}],
[new Date("2016-01-20T05:32:17.000Z").getTime(), 92.4, {}],
[new Date("2016-01-20T05:32:47.000Z").getTime(), 90.1, {}],
[new Date("2016-01-20T05:33:17.000Z").getTime(), 89.7, {}],
[new Date("2016-01-20T05:33:47.000Z").getTime(), 91.9, {}],
[new Date("2016-01-20T05:34:17.000Z").getTime(), 85.5, {}],
[new Date("2016-01-20T05:34:47.000Z").getTime(), 93.9, {}],
[new Date("2016-01-20T05:35:17.000Z").getTime(), 94.8, {}],
[new Date("2016-01-20T05:35:47.000Z").getTime(), 93.9, {}],
[new Date("2016-01-20T05:36:17.000Z").getTime(), 92.7, {}],
[new Date("2016-01-20T05:36:47.000Z").getTime(), 95.7, {}],
[new Date("2016-01-20T05:37:17.000Z").getTime(), 92.9, {}],
[new Date("2016-01-20T05:37:47.000Z").getTime(), 93.5, {}],
[new Date("2016-01-20T05:38:17.000Z").getTime(), 93.4, {}],
[new Date("2016-01-20T05:38:47.000Z").getTime(), 93.8, {}],
[new Date("2016-01-20T05:39:17.000Z").getTime(), 93.0, {}],
[new Date("2016-01-20T05:39:47.000Z").getTime(), 93.1, {}],
[new Date("2016-01-20T05:40:17.000Z").getTime(), 93.8, {}],
[new Date("2016-01-20T05:40:47.000Z").getTime(), 93.0, {}],
[new Date("2016-01-20T05:41:17.000Z").getTime(), 93.9, {}],
[new Date("2016-01-20T05:41:47.000Z").getTime(), 93.9, {}],
[new Date("2016-01-20T05:42:17.000Z").getTime(), 92.8, {}],
[new Date("2016-01-20T05:42:47.000Z").getTime(), 92.9, {}],
[new Date("2016-01-20T05:43:17.000Z").getTime(), 93.8, {}],
[new Date("2016-01-20T05:43:47.000Z").getTime(), 93.0, {}],
[new Date("2016-01-20T05:44:17.000Z").getTime(), 93.7, {}],
[new Date("2016-01-20T05:44:47.000Z").getTime(), 93.0, {}],
[new Date("2016-01-20T05:45:17.000Z").getTime(), 93.9, {}],
[new Date("2016-01-20T05:45:47.000Z").getTime(), 93.0, {}],
[new Date("2016-01-20T05:46:17.000Z").getTime(), 93.8, {}],
[new Date("2016-01-20T05:46:47.000Z").getTime(), 96.0, {}],
[new Date("2016-01-20T05:47:17.000Z").getTime(), 92.7, {}],
[new Date("2016-01-20T05:47:47.000Z").getTime(), 92.2, {}],
[new Date("2016-01-20T05:48:17.000Z").getTime(), 92.8, {}],
[new Date("2016-01-20T05:48:47.000Z").getTime(), 92.9, {}],
[new Date("2016-01-20T05:49:17.000Z").getTime(), 92.7, {}],
[new Date("2016-01-20T05:49:47.000Z").getTime(), 92.9, {}],
[new Date("2016-01-20T05:50:18.000Z").getTime(), 93.7, {}],
[new Date("2016-01-20T05:50:48.000Z").getTime(), 93.8, {}],
[new Date("2016-01-20T05:51:18.000Z").getTime(), 92.7, {}],
[new Date("2016-01-20T05:51:48.000Z").getTime(), 92.9, {}],
[new Date("2016-01-20T05:52:18.000Z").getTime(), 92.5, {}],
[new Date("2016-01-20T05:52:48.000Z").getTime(), 94.9, {}],
[new Date("2016-01-20T05:53:18.000Z").getTime(), 94.8, {}],
[new Date("2016-01-20T05:53:48.000Z").getTime(), 94.9, {}],
[new Date("2016-01-20T05:54:18.000Z").getTime(), 94.7, {}],
[new Date("2016-01-20T05:54:48.000Z").getTime(), 94.9, {}],
[new Date("2016-01-20T05:55:18.000Z").getTime(), 94.8, {}],
[new Date("2016-01-20T05:55:48.000Z").getTime(), 93.8, {}],
[new Date("2016-01-20T05:56:18.000Z").getTime(), 94.6, {}],
[new Date("2016-01-20T05:56:48.000Z").getTime(), 94.7, {}],
[new Date("2016-01-20T05:57:18.000Z").getTime(), 93.7, {}],
[new Date("2016-01-20T05:57:48.000Z").getTime(), 93.8, {}],
[new Date("2016-01-20T05:58:18.000Z").getTime(), 93.7, {}],
[new Date("2016-01-20T05:58:48.000Z").getTime(), 93.7, {}],
[new Date("2016-01-20T05:59:18.000Z").getTime(), 93.6, {}],
[new Date("2016-01-20T05:59:48.000Z").getTime(), 93.8, {}],
[new Date("2016-01-20T06:00:18.000Z").getTime(), 93.7, {}],
[new Date("2016-01-20T06:00:48.000Z").getTime(), 93.7, {}],
[new Date("2016-01-20T06:01:18.000Z").getTime(), 93.6, {}],
[new Date("2016-01-20T06:01:48.000Z").getTime(), 94.7, {}],
[new Date("2016-01-20T06:02:18.000Z").getTime(), 94.8, {}],
[new Date("2016-01-20T06:02:48.000Z").getTime(), 94.8, {}],
[new Date("2016-01-20T06:03:18.000Z").getTime(), 94.8, {}],
[new Date("2016-01-20T06:03:48.000Z").getTime(), 94.8, {}],
[new Date("2016-01-20T06:04:18.000Z").getTime(), 94.8, {}],
[new Date("2016-01-20T06:04:48.000Z").getTime(), 94.8, {}],
[new Date("2016-01-20T06:05:18.000Z").getTime(), 94.7, {}],
[new Date("2016-01-20T06:05:48.000Z").getTime(), 94.7, {}]
],
label: "b"
}]
var x = d3.scaleTime()
.range([0, width]);
var ary = [];
data.forEach(function (d) {
ary.push(d.data);
});
var x = d3.scaleTime()
.domain(d3.extent(d3.merge(ary), function (d) {
return d[0];
}))
.range([margin.left * 2, width - 30]).nice();
var y = d3.scaleLinear()
.domain(d3.extent(d3.merge(ary), function (d) {
return d[1];
}))
.range([height, margin.left * 4 - 10])
.nice();
var xAxis = d3.axisBottom(x).tickFormat(d3.timeFormat('%-I:%M %p'));
var yAxis = d3.axisLeft(y).tickFormat(d3.format('.0s'));
var line = d3.line()
.x(d => x(d[0]))
.y(d => y(d[1]));
svg.append("g")
.attr("class", "x axis axis-x")
.attr('transform', `translate(0, ${height})`)
.style('text-anchor', 'middle')
.call(xAxis);
svg.append("g")
.attr("class", "y axis axis-y")
.attr('transform', `translate(${margin.left * 2}, 0)`)
.call(yAxis);
var series = svg.selectAll(".series")
.data(data)
.enter().append("g")
.attr("class", "series");
series.append("path")
.attr("class", "line")
.attr("d", d => {
return line(d['data']);
})
.style("stroke", (d, i) => {
return z[i];
});
series.append("text")
.datum(function (d) {
return {
label: d['label'],
data: d['data'][d['data'].length - 1]
};
})
.attr("transform", function (d) {
return "translate(" + x((d.data[0])) + "," + y(d.data[1]) + ")";
})
.attr("x", 3)
.attr("dy", ".35em");
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(data)
.enter()
.append("g")
.attr("class", "mouse-per-line");
mousePerLine.append("circle")
.attr("r", 7)
.style("stroke", (d, i) => {
return z[i];
})
.style("fill", "none")
.style("stroke-width", "1px")
.style("opacity", "0");
mousePerLine.append("rect")
.attr("x", 0)
.attr("y", -10)
.attr("width", 85)
.attr("height", 35)
.style("stroke", (d, i) => {
return z[i];
})
.attr("class", "tooltip-container")
.style("fill", "red")
.style("opacity", "0")
.style("stroke-width", "1px");
mousePerLine.append("text").attr("id", "x-text")
.attr("transform", "translate(10,3)");
mousePerLine.append("text").attr("id", "y-text")
.attr("transform", "translate(10,23)");
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 rect")
.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");
d3.selectAll(".mouse-per-line rect")
.style("opacity", "0.2");
})
.on('mousemove', function () { // mouse moving over canvas
var mouse = d3.mouse(d3.event.target);
d3.select(".mouse-line")
.attr("d", function () {
var d = "M" + mouse[0] + "," + 360;
d += " " + mouse[0] + "," + 0;
return d;
});
d3.selectAll(".mouse-per-line")
.attr("transform", function (d, i) {
// console.log(this._width / mouse[0])
var xDate = x.invert(mouse[0]),
bisect = d3.bisector(function (d) {
return d[0];
}).right;
var idx = bisect(d.data, xDate);
var beginning = 0,
end = lines[i].getTotalLength(),
target = null;
while (true) {
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(this).select('#y-text')
.text("y: " + y.invert(pos.y).toFixed(2));
d3.select(this).select('#x-text')
.text("x: " + parseDate(xDate));
return "translate(" + mouse[0] + "," + pos.y + ")";
});
});
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment