Skip to content

Instantly share code, notes, and snippets.

@TommyCoin80
Last active Jan 25, 2018
Embed
What would you like to do?
Reusable D3 Tooltip
license: mit

This is my first attempt at a reusable function for creating tooltips on D3 charts.

Built with blockbuilder.org

<!DOCTYPE html>
<meta charset="utf-8">
<head>
<link href='https://fonts.googleapis.com/css?family=Play' rel='stylesheet' type='text/css'>
<style>
body {
margin:auto;
font-family: 'Play', sans-serif;
}
text {
font-family: 'Play', sans-serif;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="tooltip.js"></script>
</head>
<body>
</body>
<script>
// Dummy Data for the chart
var data = [
{name:'A',count:64,description:"This is column A!"},
{name:'B',count:32,description:"This is column B!"},
{name:'C',count:16,description:"This is column C!"},
{name:'D',count:8,description:"This is column D!"},
{name:'E',count:4,description:"This is column E!"},
{name:'F',count:2,description:"This is column F!"},
];
// Draw a basic bar chart
var margin = {top:20, left:50, bottom:50, right:20};
var height = 500 - margin.top - margin.bottom,
width = 960 - margin.left - margin.right;
var svg = d3.select("body")
.append("svg")
.attr("height", height + margin.top + margin.bottom)
.attr("width", width + margin.left + margin.right)
.style("cursor","default")
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleBand()
.domain(data.map(function(d) { return d.name;}))
.range([0,width])
.padding(.1);
var y = d3.scaleLinear()
.domain([0,70])
.range([height,0]);
var color = d3.scaleOrdinal(d3.schemeCategory10)
.domain(x.domain())
var tooltip = d3.tooltip() // returns the tooltip function
.extent([[0,0],[width,height]]) // tells the tooltip how much area it has to work with
.tips(["name","description"],["Bar Name: ","Description: "]) // tells the tooltip which properties to display in the tip and what to label thme
.fontSize(12) // sets the font size for the tooltip
.padding([8,4]) // sets the amount of padding in the tooltip rectangle
.margin([10,10]); // set the distance H and V to keep the tooltip from the mouse pointer
svg.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("x", function(d) { return x(d.name)})
.attr("y", function(d) { return y(d.count)})
.attr("width", x.bandwidth())
.attr("height", function(d) { return height - y(d.count)})
.attr("fill", function(d) { return color(d.name)})
.each(tooltip.events); // attaches the tooltips mouseenter/leave/move events but does not overwrite previously attached events
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
svg.append("g")
.call(d3.axisLeft(y));
svg.call(tooltip) // draws the tooltip;
</script>
d3.tooltip = function() {
var frameOfReference,
extent = new Array([[0,0],[960,500]]),
tips = new Array([]),
tipNames = new Array([]),
tipFormats = new Array([]),
margin = new Array([10,10]),
padding = new Array([4,4]),
translation = new Array([0,0]),
tooltipDims = new Array([0,0]),
fontSize = 12;
var tooltipGroup = d3.select(null),
tooltipRect = d3.select(null),
tooltipText = d3.select(null);
var tooltip = function(context) {
tooltipGroup.remove();
frameOfReference = context.node()
tooltipGroup = context.append("g")
.classed("d3Tooltip",true)
.style("display","none");
tooltipRect = tooltipGroup.append("rect")
.attr("width",100)
.attr("height",100)
.style("fill","black")
.style("fill-opacity",.70)
tooltipText = tooltipGroup.append("text")
.style("fill", "white")
.style("font-size",fontSize);
};
function displayTooltip(d) {
tooltipGroup.style("display",null);
tooltipText.selectAll("tspan")
.remove();
tooltipText.attr("y", padding[1])
.selectAll("tspan")
.data(tips)
.enter()
.append("tspan")
.attr("x",padding[0])
.attr("dy",fontSize*.9)
.text(function(e,i) {
var val;
if(typeof d[e] == 'undefined') {
// check the 'data' property too, for voronoi mouseover events
val = d.data[e];
} else {
val = d[e];
}
if(tipFormats[i]) {
val = tipFormats[i](val);
}
return tipNames[i] + val;
});
updateTooltipDims();
tooltipRect.attr("width", tooltipDims[0])
.attr("height", tooltipDims[1])
updateTranslation();
tooltipGroup.attr("transform","translate(" + translation[0] + "," + translation[1] + ")")
}
function hideTooltip() {
tooltipGroup.style("display","none")
}
function moveTooltip() {
updateTranslation();
tooltipGroup.attr("transform","translate(" + translation[0] + "," + translation[1] + ")");
}
tooltip.events = function() {
var me = d3.select(this).on("mouseenter") || function() {};
var ml = d3.select(this).on("mouseleave") || function() {};
var mm = d3.select(this).on("mousemove") || function() {};
d3.select(this)
.on("mouseenter", function(d,i) { me(d,i); displayTooltip(d,i);})
.on("mouseleave", function(d,i) { ml(d,i); hideTooltip(d,i); })
.on("mousemove", function(d,i) { mm(d,i); moveTooltip(d,i)});
}
tooltip.extent = function(_extent){
extent = _extent || extent;
return tooltip;
}
tooltip.fontSize = function(_fontSize) {
fontSize = _fontSize || fontSize;
return tooltip;
}
tooltip.margin = function(_margin) {
margin = _margin || margin;
return tooltip;
}
tooltip.padding = function(_padding) {
padding = _padding || padding;
return tooltip;
}
tooltip.tips = function(_tips,_tipNames,_tipFormats) {
tips = _tips || tips;
tipNames = _tipNames || tips;
tipFormats = _tipFormats || tips.map(function(d) { return null});
return tooltip;
}
function updateTooltipDims() {
var bb = tooltipText.node().getBBox();
tooltipDims = [bb.width + padding[0]*2, bb.height + padding[1]*2];
}
function updateTranslation() {
var mouseCoordinates = d3.mouse(frameOfReference);
var quad = [0,0];
if(mouseCoordinates[0] > (extent[1][0] - extent[0][0])/2) quad[0] = 1;
if(mouseCoordinates[1] > (extent[1][1] - extent[0][1])/2) quad[1] = 1;
if(quad[0] == 1) {
translation[0] = mouseCoordinates[0] - tooltipDims[0] - margin[0];
} else {
translation[0] = mouseCoordinates[0] + margin[0];
}
if(quad[1] == 1) {
translation[1] = mouseCoordinates[1] - tooltipDims[1] - margin[1];
} else {
translation[1] = mouseCoordinates[1] + margin[1];
}
}
return tooltip;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment